/**
 * 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.cxf.tools.common.toolspec.parser;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.tools.common.toolspec.Tool;
import org.apache.cxf.tools.common.toolspec.ToolSpec;

public class CommandLineParser {

    private static final Logger LOG = LogUtils.getL7dLogger(CommandLineParser.class);
    private ToolSpec toolspec;

    public CommandLineParser(ToolSpec ts) {
        this.toolspec = ts;
    }

    public void setToolSpec(ToolSpec ts) {
        this.toolspec = ts;
    }

    public static String[] getArgsFromString(String s) {
        StringTokenizer toker = new StringTokenizer(s);
        List<Object> res = new ArrayList<>();

        while (toker.hasMoreTokens()) {
            res.add(toker.nextToken());
        }
        return res.toArray(new String[0]);
    }

    public CommandDocument parseArguments(String args) throws BadUsageException, IOException {
        return parseArguments(getArgsFromString(args));
    }

    public CommandDocument parseArguments(String[] args) throws BadUsageException, IOException {

        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Parsing arguments: " + String.join(" ", args));
        }

        if (toolspec == null) {
            throw new IllegalStateException("No schema known- call to acceptSchema() must be made and must succeed");
        }

        // Create a result document

        final Document resultDoc = DOMUtils.newDocument();

        Element commandEl = resultDoc.createElementNS("http://cxf.apache.org/Xutil/Command", "command");

        Attr attr =
            commandEl.getOwnerDocument().createAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
                                                                   "xsi:schemaLocation");
        attr.setValue("http://cxf.apache.org/Xutil/Command http://cxf.apache.org/schema/xutil/commnad.xsd");
        commandEl.setAttributeNodeNS(attr);
        commandEl.setAttribute("xmlns", "http://cxf.apache.org/Xutil/Command");
        commandEl.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
        resultDoc.appendChild(commandEl);

        TokenInputStream tokens = new TokenInputStream(args);

        // for all form elements...
        Element usage = toolspec.getUsage();

        List<Element> usageForms = toolspec.getUsageForms();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Found " + usageForms.size()
                      + " alternative forms of usage, will use default form");
        }
        if (!usageForms.isEmpty()) {
            ErrorVisitor errors = new ErrorVisitor();

            for (Element elem : usageForms) {
                Form form = new Form(elem);

                int pos = tokens.getPosition();

                if (form.accept(tokens, commandEl, errors)) {
                    commandEl.setAttribute("form", form.getName());
                    break;
                }
                // if no more left then return null;
                tokens.setPosition(pos);

                if (elem.getNextSibling() == null) {
                    if (LOG.isLoggable(Level.INFO)) {
                        LOG.info("No more forms left to try, returning null");
                    }
                    throwUsage(errors);
                }


            }
/*
            for (int i = 0; i < usageForms.getLength(); i++) {
                Form form = new Form((Element)usageForms.item(i));

                int pos = tokens.getPosition();

                if (form.accept(tokens, commandEl, errors)) {
                    commandEl.setAttribute("form", form.getName());
                    break;
                } else {
                    // if no more left then return null;
                    tokens.setPosition(pos);
                    if (i == usageForms.getLength() - 1) {
                        if (LOG.isLoggable(Level.INFO)) {
                            LOG.info("No more forms left to try, returning null");
                        }
                        throwUsage(errors);
                    }
                }
            }
*/
        } else {
            ErrorVisitor errors = new ErrorVisitor();
            Form form = new Form(usage);

            if (!form.accept(tokens, commandEl, errors)) {
                throwUsage(errors);
            }
        }

        // output the result document
        if (LOG.isLoggable(Level.FINE)) {
            try {
                TransformerFactory transformerFactory = TransformerFactory.newInstance();
                transformerFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
                Transformer serializer = transformerFactory.newTransformer(
                                    new StreamSource(Tool.class
                                        .getResourceAsStream("indent-no-xml-declaration.xsl")));

                try (OutputStream os = new ByteArrayOutputStream()) {
                    serializer.transform(new DOMSource(resultDoc), new StreamResult(os));
                    LOG.fine(os.toString());
                }
            } catch (Exception ex) {
                LOG.log(Level.SEVERE, "ERROR_SERIALIZE_COMMAND_MSG", ex);
            }
        }

        return new CommandDocument(toolspec, resultDoc);
    }

    public void throwUsage(ErrorVisitor errors) throws BadUsageException, IOException {
        try {
            throw new BadUsageException(getUsage(), errors);
        } catch (TransformerException ex) {
            LOG.log(Level.SEVERE, "CANNOT_GET_USAGE_MSG", ex);
            throw new BadUsageException(errors);
        }
    }

    public String getUsage() throws TransformerException, IOException {
        // REVISIT: style usage document into a form more readily output as a
        // usage message
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream in = getClass().getResourceAsStream("usage.xsl")) {

            toolspec.transform(in, baos);
            return baos.toString();
        }
    }

    public String getDetailedUsage() throws TransformerException, IOException {
        // REVISIT: style usage document into a form more readily output as a
        // usage message
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream is = getClass().getResourceAsStream("detailedUsage.xsl")) {
            toolspec.transform(is, baos);
            return baos.toString();
        }
    }

    public String getFormattedDetailedUsage() throws TransformerException, IOException {
        final String usage;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream is = getClass().getResourceAsStream("detailedUsage.xsl")) {
            toolspec.transform(is, baos);
            usage = baos.toString();
        }
        // we use the following pattern to format usage
        // |-------|-options|------|description-----------------|
        // before option white space size is 7
        int beforeOptSpan = 3;
        // option length is 8
        int optSize = 12;
        // after option white space size is 6
        int afterOptLen = 6;
        int totalLen = 80;
        int optSpan = optSize + afterOptLen - 1;
        int beforeDesSpan = beforeOptSpan + optSpan + 1;
        String lineSeparator = System.getProperty("line.separator");
        String[] originalStrs = usage.split(lineSeparator);
        StringBuilder strbuffer = new StringBuilder();
        for (int j = 0; j < originalStrs.length - 1; j = j + 2) {
            int optionLen = originalStrs[j].length();
            addWhiteNamespace(strbuffer, beforeOptSpan);
            if (optionLen <= optSpan) {
                // && beforeOptSpan + optionLen + optSpan + desLen <= totalLen -
                // 1) {

                strbuffer.append(originalStrs[j]);
                addWhiteNamespace(strbuffer, optSpan - originalStrs[j].length());
                strbuffer.append(' ');
                if (originalStrs[j + 1].length() > totalLen - beforeDesSpan) {
                    int lastIdx = totalLen - beforeDesSpan;
                    int lastIdx2 = splitAndAppendText(strbuffer, originalStrs[j + 1], 0, lastIdx);
                    originalStrs[j + 1] = originalStrs[j + 1].substring(lastIdx2);
                    strbuffer.append(lineSeparator);
                } else {
                    strbuffer.append(originalStrs[j + 1]);
                    strbuffer.append(lineSeparator);
                    originalStrs[j + 1] = "";
                }
            } else {
                strbuffer.append(originalStrs[j]);
                strbuffer.append(lineSeparator);
            }
            String tmpStr = originalStrs[j + 1];

            for (int i = 0; i < tmpStr.length(); i = i + (totalLen - beforeDesSpan)) {
                if (i + totalLen - beforeDesSpan < tmpStr.length()) {
                    addWhiteNamespace(strbuffer, beforeDesSpan);
                    int lastIdx = i + totalLen - beforeDesSpan;
                    int lastIdx2 = splitAndAppendText(strbuffer, tmpStr, i, lastIdx);
                    i += lastIdx2 - lastIdx;
                    strbuffer.append(lineSeparator);
                } else {
                    addWhiteNamespace(strbuffer, beforeDesSpan);
                    strbuffer.append(tmpStr.substring(i));
                    strbuffer.append(lineSeparator);
                }
            }
            strbuffer.append(lineSeparator);

        }

        return strbuffer.toString();
    }
    private int splitAndAppendText(StringBuilder buffer, String tmpStr, int idx, int lastIdx) {
        int origLast = lastIdx;
        while (lastIdx > idx && !Character.isWhitespace(tmpStr.charAt(lastIdx))) {
            --lastIdx;
        }
        if (lastIdx == idx) {
            lastIdx = origLast;
        }
        buffer.append(tmpStr.substring(idx, lastIdx));

        if (Character.isWhitespace(tmpStr.charAt(lastIdx))) {
            lastIdx++;
        }
        return lastIdx;
    }

    private void addWhiteNamespace(StringBuilder strbuffer, int count) {

        for (int i = 0; i < count; i++) {
            strbuffer.append(' ');
        }
    }

    public String getDetailedUsage(String id) {
        String result = null;
        Element element = toolspec.getElementById(id);

        List<Element> annotations = DOMUtils.findAllElementsByTagNameNS(element,
                                                                     Tool.TOOL_SPEC_PUBLIC_ID,
                                                                     "annotation");


        if ((annotations != null) && !annotations.isEmpty()) {
            result = annotations.get(0).getFirstChild().getNodeValue();
        }
        return result;
    }

    public String getToolUsage() {
        return toolspec.getAnnotation();
    }

}
