/*
 * Copyright 2011-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.stepfunctions.builder;

import com.amazonaws.SdkClientException;
import com.amazonaws.services.stepfunctions.builder.internal.Buildable;
import com.amazonaws.services.stepfunctions.builder.internal.DateModule;
import com.amazonaws.services.stepfunctions.builder.internal.PropertyNames;
import com.amazonaws.services.stepfunctions.builder.internal.validation.StateMachineValidator;
import com.amazonaws.services.stepfunctions.builder.states.State;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Represents a StepFunctions state machine. A state machine must have at least one state.
 *
 * @see <a href="https://states-language.net/spec.html#toplevelfields">https://states-language.net/spec.html#toplevelfields</a>
 */
public final class StateMachine {

    private static final ObjectMapper MAPPER = new ObjectMapper()
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .registerModule(DateModule.INSTANCE);

    @JsonProperty(PropertyNames.COMMENT)
    private final String comment;

    @JsonProperty(PropertyNames.START_AT)
    private final String startAt;

    @JsonProperty(PropertyNames.TIMEOUT_SECONDS)
    private final Integer timeoutSeconds;

    @JsonProperty(PropertyNames.STATES)
    private final Map<String, State> states;

    private StateMachine(Builder builder) {
        this.comment = builder.comment;
        this.startAt = builder.startAt;
        this.timeoutSeconds = builder.timeoutSeconds;
        this.states = Buildable.Utils.build(builder.states);
    }

    /**
     * @return Human readable description for the state machine.
     */
    public String getComment() {
        return comment;
    }

    /**
     * @return Name of the state to start execution at.
     */
    public String getStartAt() {
        return startAt;
    }

    /**
     * @return Timeout, in seconds, that a state machine is allowed to run. If the machine execution runs longer than this timeout
     * the execution fails with a {@link ErrorCodes#TIMEOUT} error
     */
    public Integer getTimeoutSeconds() {
        return timeoutSeconds;
    }

    /**
     * @return The states for this state machine.
     */
    public Map<String, State> getStates() {
        return states;
    }

    /**
     * @return Compact JSON representation of this StateMachine. May be provided to API calls (see {@link
     * com.amazonaws.services.stepfunctions.AWSStepFunctions}).
     */
    public String toJson() {
        try {
            return MAPPER.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            throw new SdkClientException("Could not serialize state machine.", e);
        }
    }

    /**
     * @return Formatted JSON representation of this StateMachine. May be provided to API calls (see {@link
     * com.amazonaws.services.stepfunctions.AWSStepFunctions}).
     */
    public String toPrettyJson() {
        try {
            return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            throw new SdkClientException("Could not serialize state machine.", e);
        }
    }

    /**
     * Deserializes a JSON representation of a state machine into a {@link StateMachine.Builder} .
     *
     * @param json JSON representing State machine.
     * @return Mutable {@link StateMachine.Builder} deserialized from JSON representation.
     */
    public static StateMachine.Builder fromJson(String json) {
        try {
            return MAPPER.readValue(json, StateMachine.Builder.class);
        } catch (IOException e) {
            throw new SdkClientException(
                    String.format("Could not deserialize state machine.\n%s", json), e);
        }
    }

    /**
     * @return Builder instance to construct a {@link StateMachine}.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Builder for a {@link StateMachine}.
     */
    public static final class Builder {

        @JsonProperty(PropertyNames.COMMENT)
        private String comment;

        @JsonProperty(PropertyNames.START_AT)
        private String startAt;

        @JsonProperty(PropertyNames.TIMEOUT_SECONDS)
        private Integer timeoutSeconds;

        @JsonProperty(PropertyNames.STATES)
        private final Map<String, State.Builder> states = new LinkedHashMap<String, State.Builder>();

        private Builder() {
        }

        /**
         * OPTIONAL. Human readable description for the state machine.
         *
         * @param comment New comment.
         * @return This object for method chaining.
         */
        public Builder comment(String comment) {
            this.comment = comment;
            return this;
        }

        /**
         * REQUIRED. Name of the state to start execution at. Must match a state name provided via {@link #state(String,
         * State.Builder)}.
         *
         * @param startAt Name of starting state.
         * @return This object for method chaining.
         */
        public Builder startAt(String startAt) {
            this.startAt = startAt;
            return this;
        }

        /**
         * OPTIONAL. Timeout, in seconds, that a state machine is allowed to run. If the machine execution runs longer than this
         * timeout the execution fails with a {@link ErrorCodes#TIMEOUT} error
         *
         * @param timeoutSeconds Timeout value.
         * @return This object for method chaining.
         */
        public Builder timeoutSeconds(Integer timeoutSeconds) {
            this.timeoutSeconds = timeoutSeconds;
            return this;
        }

        /**
         * REQUIRED. Adds a new state to the state machine. A state machine MUST have at least one state.
         *
         * @param stateName    Name of the state
         * @param stateBuilder Instance of {@link com.amazonaws.services.stepfunctions.builder.states.State.Builder}. Note that
         *                     the {@link State}
         *                     object is not built until the {@link StateMachine} is built so any modifications on the state
         *                     builder will be reflected in this object.
         * @return This object for method chaining.
         */
        public Builder state(String stateName, State.Builder stateBuilder) {
            this.states.put(stateName, stateBuilder);
            return this;
        }

        /**
         * @return An immutable {@link StateMachine} object that can be transformed to JSON via {@link StateMachine#toJson()}.
         */
        public StateMachine build() {
            return new StateMachineValidator(new StateMachine(this)).validate();
        }
    }
}
