/*
 * Copyright 2005 Sascha Weinreuter
 *
 * Licensed 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.intellij.plugins.xslt.run.rt;

import org.xml.sax.SAXParseException;

import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

/** @noinspection CallToPrintStackTrace, UseOfSystemOutOrSystemErr, IOResourceOpenedButNotSafelyClosed, SocketOpenedButNotSafelyClosed */
public final class XSLTRunner implements XSLTMain {

    private XSLTRunner() {
    }

    public static void main(String[] args) throws Throwable {
        final XSLTMain main = loadMain();

        TransformerFactory transformerFactory;
        try {
            transformerFactory = main.createTransformerFactory();
        } catch (AbstractMethodError e) {
            // old debugger
            transformerFactory = createTransformerFactoryStatic();
        } catch (ClassNotFoundException e) {
            transformerFactory = createTransformerFactoryStatic();
        }

        final String uriResolverClass = System.getProperty("xslt.uri-resolver");
        if (uriResolverClass != null) {
            transformerFactory.setURIResolver((URIResolver)Class.forName(uriResolverClass).newInstance());
        }

        final boolean[] trouble = new boolean[]{ false };
        final MyErrorListener listener = new MyErrorListener(trouble);
        final boolean isSmartErrorHandling = System.getProperty("xslt.smart-error-handling", "false").equals("true");
        if (isSmartErrorHandling) {
            transformerFactory.setErrorListener(listener);
        }

        final File xslt = new File(System.getProperty("xslt.file"));
        try {
            final Transformer transformer = transformerFactory.newTransformer(new StreamSource(xslt));
            if (transformer != null && !trouble[0]) {
                final Enumeration props = System.getProperties().keys();
                while (props.hasMoreElements()) {
                    String s = (String)props.nextElement();
                    if (s.startsWith("xslt.param.")) {
                        final String name = s.substring("xslt.param.".length());
                        final String value = System.getProperty(s);
                        transformer.setParameter(name, value);
                    }
                }

                final File input = new File(System.getProperty("xslt.input"));
                final String out = System.getProperty("xslt.output");
                OutputStream fileStream;
                if (out != null) {
                  final File output = new File(out);
                  fileStream = new BufferedOutputStream(new FileOutputStream(output));
                } else {
                  fileStream = null;
                }
                final StreamResult result;

                final Integer _port = Integer.getInteger("xslt.listen-port", -1);
                final int port = _port.intValue();
                if (port != -1) {
                    // block until IDEA connects
                    try {
                        final ServerSocket serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
                        serverSocket.setSoTimeout(Integer.getInteger("xslt.listen-timeout", 5000).intValue());
                        final Socket socket = serverSocket.accept();
                        final BufferedOutputStream socketStream = new BufferedOutputStream(socket.getOutputStream(), 16);

                        if (out != null) {
                            result = new StreamResult(new ForkedOutputStream(new OutputStream[]{ socketStream, fileStream }));
                        } else {
                            result = new StreamResult(new OutputStreamWriter(socketStream, StandardCharsets.UTF_8));
                        }
                    } catch (SocketTimeoutException ignored) {
                        System.err.println("Plugin did not connect to runner within timeout. Run aborted.");
                        return;
                    }
                } else {
                    final String encoding = System.getProperty("file.encoding");
                    if (encoding != null) {
                        // ensure proper encoding in xml declaration
                        transformer.setOutputProperty("encoding", encoding);
                        if (out != null) {
                          result = new StreamResult(new OutputStreamWriter(new ForkedOutputStream(new OutputStream[]{System.out, fileStream}), encoding));
                        } else {
                          result = new StreamResult(new OutputStreamWriter(System.out, encoding));
                        }
                      } else {
                        if (out != null) {
                          result = new StreamResult(new ForkedOutputStream(new OutputStream[]{ System.out, fileStream }));
                        } else {
                          result = new StreamResult(System.out);
                        }
                      }
                }

                Runtime.getRuntime().addShutdownHook(new Thread("XSLT runner") {
                  @Override
                  public void run() {
                    try {
                      final Writer out = result.getWriter();
                      if (out != null) {
                        out.flush();
                        out.close();
                      } else if (result.getOutputStream() != null) {
                        result.getOutputStream().flush();
                        result.getOutputStream().close();
                      }
                    } catch (IOException e) {
                      // no chance to fix...
                    }
                  }
                });

                main.start(transformer, new StreamSource(input), result);
            }
        } catch (TransformerException e) {
            if (isSmartErrorHandling) {
                listener.error(e);
            } else {
                throw e;
            }
        } catch (Throwable t) {
            t.printStackTrace();
            System.exit(1);
        }
    }

    @Override
    public TransformerFactory createTransformerFactory() throws Exception {
        return createTransformerFactoryStatic();
    }

    public static TransformerFactory createTransformerFactoryStatic() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        final String factoryClass = System.getProperty("xslt.transformer-factory");
        if (factoryClass != null) {
            return (TransformerFactory)Class.forName(factoryClass).newInstance();
        } else {
            return TransformerFactory.newInstance();
        }
    }

    @Override
    public void start(Transformer transformer, Source source, Result result) throws TransformerException {
        transformer.transform(source, result);
    }

    private static XSLTMain loadMain() {
        final String mainClass = System.getProperty("xslt.main");
        if (mainClass == null) {
            return new XSLTRunner();
        }

        try {
            return (XSLTMain)Class.forName(mainClass).newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new AssertionError(e);
        }
    }

    /** @noinspection UseOfSystemOutOrSystemErr*/
    private static class MyErrorListener implements ErrorListener {
        private final Set myMessages = new HashSet();
        private final boolean[] myTrouble;

        MyErrorListener(boolean[] trouble) {
            myTrouble = trouble;
        }

        @Override
        public void warning(TransformerException exception) {
            handleException(exception, "WARNING");
        }

        @Override
        public void error(TransformerException exception) {
            handleException(exception, "ERROR");
            myTrouble[0] = true;
        }

        @Override
        public void fatalError(TransformerException exception) {
            handleException(exception, "FATAL");
            myTrouble[0] = true;
        }

        private void handleException(TransformerException exception, String type) {
            final String message = getMessage(exception);
            if (!myMessages.contains(message)) {
                System.err.println("[" + type + "]: " + message);
                myMessages.add(message);
            }
        }

        private static String getMessage(TransformerException exception) {
            final SourceLocator[] locators = new SourceLocator[]{ exception.getLocator() };
            final String[] messages = new String[1];
            findLocator(exception, locators, messages);

            final SourceLocator locator = locators[0];
            if (locator != null) {
                final String systemId = locator.getSystemId();
                if (systemId != null) {
                    String s = systemId.replaceAll(" ", "%20") + ": ";
                    final int lineNumber = locator.getLineNumber();
                    if (lineNumber != -1) {
                        s += "line " + lineNumber + ": ";
                        final int columnNumber = locator.getColumnNumber();
                        if (columnNumber != -1) {
                            s += "column " + columnNumber + ": ";
                        }
                    }
                    return s + (messages[0] != null ? messages[0] : exception.getMessage());
                }
            }
            return messages[0] != null ? messages[0] : exception.getMessage();
        }

        private static void findLocator(Throwable exception, SourceLocator[] locators, String[] messages) {
            if (exception instanceof TransformerException) {
                final TransformerException t = (TransformerException)exception;

                if (t.getLocator() != null) {
                    messages[0] = t.getMessage();
                    locators[0] = t.getLocator();
                } else if (exception.getCause() != null) {
                    findLocator(exception.getCause(), locators, messages);
                }
            } else if (exception instanceof SAXParseException) {
                final SAXParseException sae = (SAXParseException)exception;

                messages[0] = sae.getMessage();
                locators[0] = new SourceLocator() {
                    @Override
                    public int getColumnNumber() {
                        return sae.getColumnNumber();
                    }

                    @Override
                    public int getLineNumber() {
                        return sae.getLineNumber();
                    }

                    @Override
                    public String getPublicId() {
                      return null;
                    }

                    @Override
                    public String getSystemId() {
                        return sae.getSystemId();
                    }
                };
            } else if (exception.getCause() != null) {
                findLocator(exception.getCause(), locators, messages);
            }

            try {
                final Throwable t = (Throwable)exception.getClass().getMethod("getException", new Class[0]).invoke(exception, new Object[0]);
                if (t != exception) {
                    findLocator(t, locators, messages);
                }
            } catch (Exception e) {
                //
            }
        }
    }

    static class ForkedOutputStream extends OutputStream {
        OutputStream[] outs;

        ForkedOutputStream(OutputStream[] out) {
            outs = out;
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
          for (int i = 0, outsLength = outs.length; i < outsLength; i++) {
            outs[i].write(b, off, len);
          }
        }

        @Override
        public void write(int b) throws IOException {
          for (int i = 0, outsLength = outs.length; i < outsLength; i++) {
            outs[i].write(b);
          }
        }

        @Override
        public void flush() throws IOException {
          for (int i = 0, outsLength = outs.length; i < outsLength; i++) {
            outs[i].flush();
          }
        }

        @Override
        public void close() throws IOException {
          for (int i = 0, outsLength = outs.length; i < outsLength; i++) {
            outs[i].close();
          }
        }
    }
}
