/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.isis.viewer.restfulobjects.applib.client;

import java.io.IOException;
import java.util.List;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status.Family;

import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
import org.apache.isis.viewer.restfulobjects.applib.LinkRepresentation;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.common.collect.Lists;

public class RepresentationWalker {

    static class Step {
        private final String key;
        private final LinkRepresentation link;
        private final JsonRepresentation body;
        private final RestfulResponse<? extends JsonRepresentation> response;
        private String error;
        private final Exception exception;

        public Step(final String key, final LinkRepresentation link, final JsonRepresentation body, final RestfulResponse<? extends JsonRepresentation> response, final String error, final Exception exception) {
            this.key = key;
            this.link = link;
            this.body = body;
            this.response = response;
            this.error = error;
            this.exception = exception;
        }

        @Override
        public String toString() {
            return "Step [key=" + key + ", link=" + (link != null ? link.getHref() : "(null)") + ", error=" + error + "]";
        }

    }

    private final RestfulClient restfulClient;
    private final List<Step> steps = Lists.newLinkedList();

    public RepresentationWalker(final RestfulClient restfulClient, final Response response) {
        this.restfulClient = restfulClient;
        final RestfulResponse<JsonRepresentation> jsonResp = RestfulResponse.of(response);

        addStep(null, null, null, jsonResp, null, null);
    }

    private Step addStep(final String key, final LinkRepresentation link, final JsonRepresentation body, final RestfulResponse<JsonRepresentation> jsonResp, final String error, final Exception ex) {
        final Step step = new Step(key, link, body, jsonResp, error, ex);
        steps.add(0, step);
        if (error != null) {
            if (jsonResp.getStatus().getFamily() != Family.SUCCESSFUL) {
                step.error = "response status code: " + jsonResp.getStatus();
            }
        }
        return step;
    }

    public void walk(final String path) {
        walk(path, null);
    }

    public void walk(final String path, final JsonRepresentation invokeBody) {
        final Step previousStep = currentStep();
        if (previousStep.error != null) {
            return;
        }

        final RestfulResponse<? extends JsonRepresentation> jsonResponse = previousStep.response;
        JsonRepresentation entity;
        try {
            entity = jsonResponse.getEntity();
        } catch (final Exception e) {
            addStep(path, null, null, null, "exception: " + e.getMessage(), e);
            return;
        }

        LinkRepresentation link;
        try {
            link = entity.getLink(path);
        } catch (final Exception e) {
            addStep(path, null, null, null, "exception: " + e.getMessage(), e);
            return;
        }
        if (link == null) {
            addStep(path, null, null, null, "no such link '" + path + "'", null);
            return;
        }

        final RestfulResponse<JsonRepresentation> response;
        try {
            if (invokeBody != null) {
                response = restfulClient.follow(link, invokeBody);
            } else {
                response = restfulClient.follow(link);
            }
        } catch (final Exception e) {
            addStep(path, link, null, null, "failed to follow link: " + e.getMessage(), e);
            return;
        }

        addStep(path, link, null, response, null, null);
    }

    /**
     * The entity returned from the previous walk.
     * 
     * <p>
     * Will return null if the previous walk returned an error.
     */
    public JsonRepresentation getEntity() throws JsonParseException, JsonMappingException, IOException {
        final Step currentStep = currentStep();
        if (currentStep.response == null || currentStep.error != null) {
            return null;
        }
        return currentStep.response.getEntity();
    }

    /**
     * The response returned from the previous walk.
     * 
     * <p>
     * Once a walk/performed has been attempted, is guaranteed to return a
     * non-null value. (Conversely, will return <tt>null</tt> immediately after
     * instantiation and prior to a walk being attempted/performed).
     */
    public RestfulResponse<?> getResponse() {
        final Step currentStep = currentStep();
        return currentStep != null ? currentStep.response : null;
    }

    /**
     * The error (if any) that occurred from the previous walk.
     */
    public String getError() {
        final Step currentStep = currentStep();
        return currentStep != null ? currentStep.error : null;
    }

    /**
     * The exception (if any) that occurred from the previous walk.
     * 
     * <p>
     * Will only ever be populated if {@link #getError()} is non-null.
     */
    public Exception getException() {
        final Step currentStep = currentStep();
        return currentStep != null ? currentStep.exception : null;
    }

    /**
     * The step that has just been walked.
     */
    private Step currentStep() {
        return steps.get(0);
    }

}
