/*
 * 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.dubbo.qos.textui;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import static java.lang.System.currentTimeMillis;
import static org.apache.dubbo.common.utils.StringUtils.EMPTY_STRING;
import static org.apache.dubbo.common.utils.StringUtils.length;
import static org.apache.dubbo.common.utils.StringUtils.repeat;

/**
 * tree
 */
public class TTree implements TComponent {

    private static final String STEP_FIRST_CHAR = "`---";
    private static final String STEP_NORMAL_CHAR = "+---";
    private static final String STEP_HAS_BOARD = "|   ";
    private static final String STEP_EMPTY_BOARD = "    ";

    // should output cost or not
    private final boolean isPrintCost;

    // tree node
    private final Node root;

    // current node
    private Node current;

    public TTree(boolean isPrintCost, String title) {
        this.root = new Node(title).markBegin().markEnd();
        this.current = root;
        this.isPrintCost = isPrintCost;
    }

    @Override
    public String rendering() {

        final StringBuilder treeSB = new StringBuilder();
        recursive(0, true, "", root, new Callback() {

            @Override
            public void callback(int deep, boolean isLast, String prefix, Node node) {

                final boolean hasChild = !node.children.isEmpty();
                final String stepString = isLast ? STEP_FIRST_CHAR : STEP_NORMAL_CHAR;
                final int stepStringLength = length(stepString);
                treeSB.append(prefix).append(stepString);

                int costPrefixLength = 0;
                if (hasChild) {
                    treeSB.append('+');
                }
                if (isPrintCost && !node.isRoot()) {
                    final String costPrefix = String.format(
                            "[%s,%sms]",
                            (node.endTimestamp - root.beginTimestamp), (node.endTimestamp - node.beginTimestamp));
                    costPrefixLength = length(costPrefix);
                    treeSB.append(costPrefix);
                }

                try (Scanner scanner = new Scanner(new StringReader(node.data.toString()))) {
                    boolean isFirst = true;
                    while (scanner.hasNextLine()) {
                        if (isFirst) {
                            treeSB.append(scanner.nextLine()).append('\n');
                            isFirst = false;
                        } else {
                            treeSB.append(prefix)
                                    .append(repeat(' ', stepStringLength))
                                    .append(hasChild ? "|" : EMPTY_STRING)
                                    .append(repeat(' ', costPrefixLength))
                                    .append(scanner.nextLine())
                                    .append(System.lineSeparator());
                        }
                    }
                }
            }
        });

        return treeSB.toString();
    }

    /**
     * recursive visit
     */
    private void recursive(int deep, boolean isLast, String prefix, Node node, Callback callback) {
        callback.callback(deep, isLast, prefix, node);
        if (!node.isLeaf()) {
            final int size = node.children.size();
            for (int index = 0; index < size; index++) {
                final boolean isLastFlag = index == size - 1;
                final String currentPrefix = isLast ? prefix + STEP_EMPTY_BOARD : prefix + STEP_HAS_BOARD;
                recursive(deep + 1, isLastFlag, currentPrefix, node.children.get(index), callback);
            }
        }
    }

    public boolean isTop() {
        return current.isRoot();
    }

    /**
     * create a branch node
     *
     * @param data node data
     * @return this
     */
    public TTree begin(Object data) {
        current = new Node(current, data);
        current.markBegin();
        return this;
    }

    public TTree begin() {
        return begin(null);
    }

    public Object get() {
        if (current.isRoot()) {
            throw new IllegalStateException("current node is root.");
        }
        return current.data;
    }

    public TTree set(Object data) {
        if (current.isRoot()) {
            throw new IllegalStateException("current node is root.");
        }
        current.data = data;
        return this;
    }

    /**
     * end a branch node
     *
     * @return this
     */
    public TTree end() {
        if (current.isRoot()) {
            throw new IllegalStateException("current node is root.");
        }
        current.markEnd();
        current = current.parent;
        return this;
    }

    /**
     * tree node
     */
    private static class Node {

        /**
         * parent node
         */
        final Node parent;

        /**
         * node data
         */
        Object data;

        /**
         * child nodes
         */
        final List<Node> children = new ArrayList<>();

        /**
         * begin timestamp
         */
        private long beginTimestamp;

        /**
         * end timestamp
         */
        private long endTimestamp;

        /**
         * construct root node
         */
        private Node(Object data) {
            this.parent = null;
            this.data = data;
        }

        /**
         * construct a regular node
         *
         * @param parent parent node
         * @param data   node data
         */
        private Node(Node parent, Object data) {
            this.parent = parent;
            this.data = data;
            parent.children.add(this);
        }

        /**
         * is the current node the root node
         *
         * @return true / false
         */
        boolean isRoot() {
            return null == parent;
        }

        /**
         * if the current node the leaf node
         *
         * @return true / false
         */
        boolean isLeaf() {
            return children.isEmpty();
        }

        Node markBegin() {
            beginTimestamp = currentTimeMillis();
            return this;
        }

        Node markEnd() {
            endTimestamp = currentTimeMillis();
            return this;
        }
    }

    /**
     * callback interface for recursive visit
     */
    private interface Callback {

        void callback(int deep, boolean isLast, String prefix, Node node);
    }
}
