/*
 * 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.felix.gogo.runtime;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.Channels;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.AbstractInterruptibleChannel;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.felix.service.command.Job.Status;
import org.apache.felix.gogo.runtime.Parser.Array;
import org.apache.felix.gogo.runtime.Parser.Executable;
import org.apache.felix.gogo.runtime.Parser.Operator;
import org.apache.felix.gogo.runtime.Parser.Pipeline;
import org.apache.felix.gogo.runtime.Parser.Program;
import org.apache.felix.gogo.runtime.Parser.Sequence;
import org.apache.felix.gogo.runtime.Parser.Statement;
import org.apache.felix.gogo.runtime.Pipe.Result;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Function;

public class Closure implements Function, Evaluate
{

    public static final String LOCATION = ".location";
    public static final String PIPE_EXCEPTION = "pipe-exception";
    private static final String DEFAULT_LOCK = ".defaultLock";

    private static final ThreadLocal<String> location = new ThreadLocal<>();

    private final CommandSessionImpl session;
    private final Closure parent;
    private final CharSequence source;
    private final Program program;
    private final Object script;

    private Token errTok;
    private Token errTok2;
    private List<Object> parms = null;
    private List<Object> parmv = null;

    public Closure(CommandSessionImpl session, Closure parent, CharSequence source) throws Exception
    {
        this.session = session;
        this.parent = parent;
        this.source = source;
        this.script = session.get("0"); // by convention, $0 is script name

        if (source instanceof Program)
        {
            program = (Program) source;
        }
        else
        {
            try
            {
                this.program = new Parser(source).program();
            }
            catch (Exception e)
            {
                throw setLocation(e);
            }
        }
    }

    public Closure(CommandSessionImpl session, Closure parent, Program program)
    {
        this.session = session;
        this.parent = parent;
        this.source = program;
        this.script = session.get("0"); // by convention, $0 is script name
        this.program = program;
    }

    public CommandSessionImpl session()
    {
        return session;
    }

    private Exception setLocation(Exception e)
    {
        if (session.get(DEFAULT_LOCK) == null)
        {
            String loc = location.get();
            if (null == loc)
            {
                loc = (null == script ? "" : script + ":");

                if (e instanceof SyntaxError)
                {
                    SyntaxError se = (SyntaxError) e;
                    loc += se.line() + "." + se.column();
                }
                else if (null != errTok)
                {
                    loc += errTok.line + "." + errTok.column;
                }

                location.set(loc);
            }
            else if (null != script && !loc.contains(":"))
            {
                location.set(script + ":" + loc);
            }

            session.put(LOCATION, location.get());
        }

        if (e instanceof EOFError)
        { // map to public exception, so interactive clients can provide more input
            EOFException eofe = new EOFException(e.getMessage());
            eofe.initCause(e);
            return eofe;
        }

        return e;
    }

    // implements Function interface
    public Object execute(CommandSession x, List<Object> values) throws Exception
    {
        return execute(x, values, null);
    }

    public Object execute(CommandSession x, List<Object> values, Channel capturingOutput) throws Exception
    {
        if (x != null && x != session)
        {
            if (x instanceof CommandSessionImpl)
            {
                return new Closure((CommandSessionImpl) x, null, program).execute(x, values, capturingOutput);
            }
            else
            {
                throw new IllegalStateException("The current session is not a Gogo session");
            }
        }
        try
        {
            location.remove();
            session.put(LOCATION, null);
            return execute(values, capturingOutput);
        }
        catch (Exception e)
        {
            throw setLocation(e);
        }
    }

    @SuppressWarnings("unchecked")
    private Object execute(List<Object> values, Channel capturingOutput) throws Exception
    {
        if (null != values)
        {
            parmv = new ArrayList<>(values);
            parms = new ArgList(parmv);
        }
        else if (null != parent)
        {
            // inherit parent closure parameters
            parmv = parent.parmv != null ? new ArrayList<>(parent.parmv) : null;
            parms = parmv != null ? new ArgList(parmv) : null;
        }
        else
        {
            // inherit session parameters
            Object args = session.get("args");
            if (null != args && args instanceof List<?>)
            {
                parmv = new ArrayList<>((List<Object>) args);
                parms = new ArgList(parmv);
            }
        }

        Result last = null;
        Operator operator = null;
        for (Iterator<Executable> iterator = program.tokens().iterator(); iterator.hasNext();)
        {
            Operator prevOperator = operator;
            Executable executable = iterator.next();
            if (iterator.hasNext()) {
                operator = (Operator) iterator.next();
            } else {
                operator = null;
            }
            if (prevOperator != null) {
                if (Token.eq("&&", prevOperator)) {
                    if (!last.isSuccess()) {
                        continue;
                    }
                }
                else if (Token.eq("||", prevOperator)) {
                    if (last.isSuccess()) {
                        continue;
                    }
                }
            }

            Channel[] streams;
            boolean[] toclose = new boolean[10];
            if (Pipe.getCurrentPipe() != null) {
                streams = Pipe.getCurrentPipe().streams.clone();
            } else {
                streams = new Channel[10];
                streams[0] = session.channels[0];
                streams[1] = new WritableByteChannelImpl((WritableByteChannel) session.channels[1]);
                streams[2] = session.channels[1] == session.channels[2]
                                ? streams[1]
                                : new WritableByteChannelImpl((WritableByteChannel) session.channels[2]);
            }
            if (capturingOutput != null) {
                streams[1] = capturingOutput;
                toclose[1] = true;
            }

            CommandSessionImpl.JobImpl job;
            if (executable instanceof Pipeline) {
                Pipeline pipeline = (Pipeline) executable;
                List<Executable> exec = pipeline.tokens();
                Token s = exec.get(0);
                Token e = exec.get(exec.size() - 1);
                Token t = program.subSequence(s.start - program.start, e.start + e.length - program.start);
                job = session().createJob(t);
                for (int i = 0; i < exec.size(); i++) {
                    Statement ex = (Statement) exec.get(i);
                    Operator op = i < exec.size() - 1 ? (Operator) exec.get(++i) : null;
                    Channel[] nstreams;
                    boolean[] ntoclose;
                    boolean endOfPipe;
                    if (i == exec.size() - 1) {
                        nstreams = streams;
                        ntoclose = toclose;
                        endOfPipe = true;
                    } else if (Token.eq("|", op)) {
                        PipedInputStream pis = new PipedInputStream();
                        PipedOutputStream pos = new PipedOutputStream(pis);
                        nstreams = streams.clone();
                        nstreams[1] = Channels.newChannel(pos);
                        ntoclose = toclose.clone();
                        ntoclose[1] = true;
                        streams[0] = Channels.newChannel(pis);
                        toclose[0] = true;
                        endOfPipe = false;
                    } else if (Token.eq("|&", op)) {
                        PipedInputStream pis = new PipedInputStream();
                        PipedOutputStream pos = new PipedOutputStream(pis);
                        nstreams = streams.clone();
                        nstreams[1] = nstreams[2] = Channels.newChannel(pos);
                        ntoclose = toclose.clone();
                        ntoclose[1] = ntoclose[2] = true;
                        streams[0] = Channels.newChannel(pis);
                        toclose[0] = true;
                        endOfPipe = false;
                    } else {
                        throw new IllegalStateException("Unrecognized pipe operator: '" + op + "'");
                    }
                    Pipe pipe = new Pipe(this, job, ex, nstreams, ntoclose, endOfPipe);
                    job.addPipe(pipe);
                }
            } else {
                job = session().createJob(executable);
                Pipe pipe = new Pipe(this, job, (Statement) executable, streams, toclose, true);
                job.addPipe(pipe);
            }

            // Start pipe in background
            if (operator != null && Token.eq("&", operator)) {
                job.start(Status.Background);
                last = new Result((Object) null);
            }
            // Start in foreground and wait for results
            else {
                last = job.start(Status.Foreground);
                if (last == null) {
                    last = new Result((Object) null);
                } else if (last.exception != null) {
                    throw last.exception;
                }
            }
        }

        return last == null ? null : last.result;
    }

    private static class WritableByteChannelImpl extends AbstractInterruptibleChannel
            implements WritableByteChannel {
        private final WritableByteChannel out;

        WritableByteChannelImpl(WritableByteChannel out) {
            this.out = out;
        }

        public int write(ByteBuffer src) throws IOException {
            if (!isOpen()) {
                throw new ClosedChannelException();
            }
            begin();
            try {
                return out.write(src);
            } finally {
                end(true);
            }
       }

        protected void implCloseChannel() {
        }
    }

    static Object eval(Object v)
    {
        String s = v.toString();
        if ("null".equals(s))
        {
            v = null;
        }
        else if ("false".equals(s))
        {
            v = false;
        }
        else if ("true".equals(s))
        {
            v = true;
        }
        else
        {
            try
            {
                v = s;
                v = Double.parseDouble(s);    // if it parses as double
                v = Long.parseLong(s);        // see whether it is integral
            }
            catch (NumberFormatException e)
            {
                // Ignore
            }
        }
        return v;
    }

    public Object eval(final Token t) throws Exception
    {
        return eval(t, true);
    }

    public Object eval(final Token t, boolean convertNumeric) throws Exception
    {
        if (t instanceof Parser.Closure)
        {
            return new Closure(session, this, ((Parser.Closure) t).program());
        }
        else if (t instanceof Sequence)
        {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Channel out = Channels.newChannel(baos);
            Object result = new Closure(session, this, ((Sequence) t).program())
                    .execute(session, parms, out);
            if (result != null) {
                return result;
            } else {
                String s = baos.toString();
                while (!s.isEmpty() && s.charAt(s.length() - 1) == '\n') {
                    s = s.substring(0, s.length() - 1);
                }
                return s;
            }
        }
        else if (t instanceof Array)
        {
            return array((Array) t);
        }
        else {
            Object v = Expander.expand(t, this);
            if (t == v)
            {
                if (convertNumeric)
                {
                    v = eval(v);
                }
            }
            return v;
        }
    }

    public Object execute(Executable executable) throws Exception
    {
        if (executable instanceof Statement)
        {
            return executeStatement((Statement) executable);
        }
        else if (executable instanceof Sequence)
        {
            return new Closure(session, this, ((Sequence) executable).program()).execute(new ArrayList<>(), null);
        }
        else
        {
            throw new IllegalStateException();
        }
    }

    /*
     * executeStatement handles the following cases:
     *    <string> '=' word // simple assignment
     *    <string> '=' word word.. // complex assignment
     *    <bareword> word.. // command invocation
     *    <object> // value of <object>
     *    <object> word.. // method call
     */
    public Object executeStatement(Statement statement) throws Exception
    {
        Object echo = session.get("echo");
        String xtrace = null;

        if (echo != null && !"false".equals(echo.toString()))
        {
            // set -x execution trace
            xtrace = "+" + statement;
            session.perr.println(xtrace);
        }

        List<Token> tokens = statement.tokens();
        if (tokens.isEmpty())
        {
            return null;
        }

        List<Object> values = new ArrayList<>();
        errTok = tokens.get(0);

        if ((tokens.size() > 3) && Token.eq("=", tokens.get(1)))
        {
            errTok2 = tokens.get(2);
        }

        for (Token t : tokens)
        {
            Object v = eval(t, values.isEmpty());

//            if ((Token.Type.EXECUTION == t.type) && (tokens.size() == 1)) {
//                return v;
//            }

            if (v instanceof ArgList)
            {
                values.addAll((ArgList) v); // explode $args array
            }
            else
            {
                values.add(v);
            }
        }

        Object cmd = values.remove(0);
        if (cmd == null)
        {
            if (values.isEmpty())
            {
                return null;
            }

            throw new RuntimeException("Command name evaluates to null: " + errTok);
        }

        if (cmd instanceof CharSequence && values.size() > 0
                && Token.eq("=", tokens.get(1)))
        {
            values.remove(0);
            String scmd = cmd.toString();
            Object value;

            if (values.size() == 0)
            {
                return session.put(scmd, null);
            }

            if (values.size() == 1)
            {
                value = values.get(0);
            }
            else
            {
                cmd = values.remove(0);
                if (null == cmd)
                {
                    throw new RuntimeException("Command name evaluates to null: "
                            + errTok2);
                }

                trace2(xtrace, cmd, values);

                value = bareword(tokens.get(2), cmd) ? executeCmd(cmd.toString(), values)
                    : executeMethod(cmd, values);
            }

            return assignment(scmd, value);
        }

        trace2(xtrace, cmd, values);

        return bareword(tokens.get(0), cmd) ? executeCmd(cmd.toString(), values)
            : executeMethod(cmd, values);
    }

    // second level expanded execution trace
    private void trace2(String trace1, Object cmd, List<Object> values)
    {
        if ("verbose".equals(session.get("echo")))
        {
            StringBuilder buf = new StringBuilder("+ " + cmd);

            for (Object value : values)
            {
                buf.append(' ');
                buf.append(value);
            }

            String trace2 = buf.toString();

            if (!trace2.equals(trace1))
            {
                session.perr.println("+" + trace2);
            }
        }
    }

    private boolean bareword(Token t, Object v) {
        return v instanceof CharSequence && Token.eq(t, (CharSequence) v);
    }

    private Object executeCmd(String scmd, List<Object> values) throws Exception
    {
        String scopedFunction = scmd;
        Object x = get(scmd);

        if (!(x instanceof Function))
        {
            if (scmd.indexOf(':') < 0)
            {
                scopedFunction = "*:" + scmd;
            }

            x = get(scopedFunction);

            if (x == null || !(x instanceof Function))
            {
                // try default command handler
                if (session.get(DEFAULT_LOCK) == null)
                {
                    x = get("default");
                    if (x == null)
                    {
                        x = get("*:default");
                    }

                    if (x instanceof Function)
                    {
                        try
                        {
                            session.put(DEFAULT_LOCK, true);
                            values.add(0, scmd);
                            return ((Function) x).execute(session, values);
                        }
                        finally
                        {
                            session.put(DEFAULT_LOCK, null);
                        }
                    }
                }

                throw new CommandNotFoundException(scmd);
            }
        }
        return ((Function) x).execute(session, values);
    }

    private Object executeMethod(Object cmd, List<Object> values) throws Exception
    {
        if (values.isEmpty())
        {
            return cmd;
        }

        boolean dot = values.size() > 1 && ".".equals(String.valueOf(values.get(0)));

        // FELIX-1473 - allow method chaining using dot pseudo-operator, e.g.
        //  (bundle 0) . loadClass java.net.InetAddress . localhost . hostname
        //  (((bundle 0) loadClass java.net.InetAddress ) localhost ) hostname
        if (dot)
        {
            Object target = cmd;
            ArrayList<Object> args = new ArrayList<>();
            values.remove(0);

            for (Object arg : values)
            {
                if (".".equals(arg))
                {
                    target = invoke(target, args.remove(0).toString(), args);
                    args.clear();
                }
                else
                {
                    args.add(arg);
                }
            }

            if (args.size() == 0)
            {
                return target;
            }

            return invoke(target, args.remove(0).toString(), args);
        }
        else if (cmd.getClass().isArray() && values.size() == 1)
        {
            Object[] cmdv = (Object[]) cmd;
            String index = values.get(0).toString();
            return "length".equals(index) ? cmdv.length : cmdv[Integer.parseInt(index)];
        }
        else
        {
            return invoke(cmd, values.remove(0).toString(), values);
        }
    }

    private Object invoke(Object target, String name, List<Object> args) throws Exception
    {
        return session.invoke(target, name, args);
    }

    private Object assignment(String name, Object value)
    {
        session.put(name, value);
        return value;
    }

    public Object expr(Token expr)
    {
        return session.expr(expr);
    }

    private Object array(Array array) throws Exception
    {
        List<Token> list = array.list();
        Map<Token, Token> map = array.map();

        if (list != null)
        {
            List<Object> olist = new ArrayList<>();
            for (Token t : list)
            {
                Object oval = eval(t);
                if (oval.getClass().isArray())
                {
                    Collections.addAll(olist, (Object[]) oval);
                }
                else if (oval instanceof ArgList)
                {
                    olist.addAll((ArgList) oval);
                }
                else
                {
                    olist.add(oval);
                }
            }
            return olist;
        }
        else
        {
            Map<Object, Object> omap = new LinkedHashMap<>();
            for (Entry<Token, Token> e : map.entrySet())
            {
                Token key = e.getKey();
                Object k = eval(key);
                if (!(k instanceof String))
                {
                    throw new SyntaxError(key.line(), key.column(),
                        "map key null or not String: " + key);
                }
                omap.put(k, eval(e.getValue()));
            }
            return omap;
        }
    }

    public Object get(String name)
    {
        if (parms != null)
        {
            if ("args".equals(name))
            {
                return parms;
            }

            if ("argv".equals(name))
            {
                return parmv;
            }

            if ("it".equals(name))
            {
                return parms.get(0);
            }

            if (name.length() == 1 && Character.isDigit(name.charAt(0)))
            {
                int i = name.charAt(0) - '0';
                if (i > 0)
                {
                    return parms.get(i - 1);
                }
            }
        }

        return session.get(name);
    }

    public Object put(String key, Object value)
    {
        return session.put(key, value);
    }

    @Override
    public Path currentDir() {
        return isSet(CommandSession.OPTION_NO_GLOB, false) ? null : session().currentDir();
    }

    @Override
    public ClassLoader classLoader() {
        return session.classLoader();
    }

    protected boolean isSet(String name, boolean def) {
        Object v = session.get(name);
        if (v instanceof Boolean) {
            return (Boolean) v;
        } else if (v != null) {
            String s = v.toString();
            return s.isEmpty() || s.equalsIgnoreCase("on")
                    || s.equalsIgnoreCase("1") || s.equalsIgnoreCase("true");
        }
        return def;
    }

    @Override
    public String toString()
    {
        return source.toString().trim().replaceAll("\n+", "\n").replaceAll(
            "([^\\\\{}(\\[])[\\s\n]*\n", "$1;").replaceAll("[ \\\\\t\n]+", " ");
    }

}
