/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.core.subst;

import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.spi.PropertyContainer;
import ch.qos.logback.core.spi.ScanException;
import ch.qos.logback.core.subst.Node.Type;
import ch.qos.logback.core.util.OptionHelper;

import java.util.List;
import java.util.Stack;

/**
 * Compiles a previously parsed Node chain into a String.
 *
 * @author Ceki G&uuml;lc&uuml;
 */
public class NodeToStringTransformer {

    public static final String CIRCULAR_VARIABLE_REFERENCE_DETECTED = "Circular variable reference detected while parsing input [";
    final Node node;
    final PropertyContainer propertyContainer0;
    final PropertyContainer propertyContainer1;

    public NodeToStringTransformer(Node node, PropertyContainer propertyContainer0,
            PropertyContainer propertyContainer1) {
        this.node = node;
        this.propertyContainer0 = propertyContainer0;
        this.propertyContainer1 = propertyContainer1;
    }

    public NodeToStringTransformer(Node node, PropertyContainer propertyContainer0) {
        this(node, propertyContainer0, null);
    }

    public static String substituteVariable(String input, PropertyContainer pc0, PropertyContainer pc1)
            throws ScanException {
        Node node = tokenizeAndParseString(input);
        NodeToStringTransformer nodeToStringTransformer = new NodeToStringTransformer(node, pc0, pc1);
        return nodeToStringTransformer.transform();
    }

    private static Node tokenizeAndParseString(String value) throws ScanException {
        Tokenizer tokenizer = new Tokenizer(value);
        List<Token> tokens = tokenizer.tokenize();
        Parser parser = new Parser(tokens);
        return parser.parse();
    }

    public String transform() throws ScanException {
        StringBuilder stringBuilder = new StringBuilder();
        compileNode(node, stringBuilder, new Stack<Node>());
        return stringBuilder.toString();
    }

    private void compileNode(Node inputNode, StringBuilder stringBuilder, Stack<Node> cycleCheckStack)
            throws ScanException {
        Node n = inputNode;
        while (n != null) {
            switch (n.type) {
            case LITERAL:
                handleLiteral(n, stringBuilder);
                break;
            case VARIABLE:
                handleVariable(n, stringBuilder, cycleCheckStack);
                break;
            }
            n = n.next;
        }
    }

    private void handleVariable(Node n, StringBuilder stringBuilder, Stack<Node> cycleCheckStack) throws ScanException {

        // Check for recursion
        if (haveVisitedNodeAlready(n, cycleCheckStack)) {
            cycleCheckStack.push(n);
            String error = constructRecursionErrorMessage(cycleCheckStack);
            throw new IllegalArgumentException(error);
        }
        cycleCheckStack.push(n);

        StringBuilder keyBuffer = new StringBuilder();
        Node payload = (Node) n.payload;
        compileNode(payload, keyBuffer, cycleCheckStack);
        String key = keyBuffer.toString();
        String value = lookupKey(key);

        // empty values are considered valid
        if (value != null) {
            Node innerNode = tokenizeAndParseString(value);
            compileNode(innerNode, stringBuilder, cycleCheckStack);
            cycleCheckStack.pop();
            return;
        }

        // empty default literal is a valid value
        if (n.defaultPart == null) {
            stringBuilder.append(key + CoreConstants.UNDEFINED_PROPERTY_SUFFIX);
            cycleCheckStack.pop();
            return;
        }

        Node defaultPart = (Node) n.defaultPart;
        StringBuilder defaultPartBuffer = new StringBuilder();
        compileNode(defaultPart, defaultPartBuffer, cycleCheckStack);
        cycleCheckStack.pop();
        String defaultVal = defaultPartBuffer.toString();
        stringBuilder.append(defaultVal);
    }

    private String lookupKey(String key) {
        String value = propertyContainer0.getProperty(key);
        if (value != null)
            return value;

        if (propertyContainer1 != null) {
            value = propertyContainer1.getProperty(key);
            if (value != null)
                return value;
        }

        value = OptionHelper.getSystemProperty(key, null);
        if (value != null)
            return value;

        value = OptionHelper.getEnv(key);
        if (value != null) {
            return value;
        }

        return null;
    }

    private void handleLiteral(Node n, StringBuilder stringBuilder) {
        stringBuilder.append((String) n.payload);
    }

    private String variableNodeValue(Node variableNode) {
        Node payload = (Node) variableNode.payload;
        if(payload == null) {
            return CoreConstants.EMPTY_STRING;
        }
        
        if(payload.type == Type.LITERAL) {
            return (String) payload.payload;
        }
        
        if(payload.type == Type.VARIABLE) {
            return " ? " + variableNodeValue(payload);
        }
        throw new IllegalStateException("unreachable code");
    }

    private String constructRecursionErrorMessage(Stack<Node> recursionNodes) {
        StringBuilder errorBuilder = new StringBuilder(CIRCULAR_VARIABLE_REFERENCE_DETECTED);

        for (Node stackNode : recursionNodes) {
            errorBuilder.append("${").append(variableNodeValue(stackNode)).append("}");
            if (recursionNodes.lastElement() != stackNode) {
                errorBuilder.append(" --> ");
            }
        }
        errorBuilder.append("]");
        return errorBuilder.toString();
    }

    /**
     * Determine if a node has already been visited already by checking the
     * cycleDetectionStack for its existence. This method is used -- rather than
     * Stack.contains() -- because we want to ignore the Node's 'next' attribute
     * when comparing for equality.
     */
    private boolean haveVisitedNodeAlready(Node node, Stack<Node> cycleDetectionStack) {
        for (Node cycleNode : cycleDetectionStack) {
            if (equalNodes(node, cycleNode)) {
                return true;
            }
        }
        return false;
    }

    private boolean equalNodes(Node node1, Node node2) {
        if (node1.type != null && !node1.type.equals(node2.type))
            return false;
        if (node1.payload != null && !node1.payload.equals(node2.payload))
            return false;
        if (node1.defaultPart != null && !node1.defaultPart.equals(node2.defaultPart))
            return false;

        return true;
    }

}
