/*
 * Copyright 2007 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.xsltDebugger.rt.engine.local.saxon;

import com.icl.saxon.Bindery;
import com.icl.saxon.Binding;
import com.icl.saxon.Context;
import com.icl.saxon.expr.*;
import com.icl.saxon.om.*;
import com.icl.saxon.output.GeneralOutputter;
import com.icl.saxon.style.ExpressionContext;
import com.icl.saxon.style.StyleElement;
import com.icl.saxon.style.XSLGeneralVariable;
import com.icl.saxon.style.XSLParam;
import org.intellij.plugins.xsltDebugger.rt.engine.Debugger;
import org.intellij.plugins.xsltDebugger.rt.engine.Value;
import org.intellij.plugins.xsltDebugger.rt.engine.local.VariableComparator;
import org.intellij.plugins.xsltDebugger.rt.engine.local.VariableImpl;
import org.w3c.dom.Node;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

class SaxonFrameImpl extends AbstractSaxonFrame<Debugger.StyleFrame, StyleElement> implements Debugger.StyleFrame {

  private static Field fGeneralUseAllowed;

  static {
    try {
      fGeneralUseAllowed = SingletonNodeSet.class.getDeclaredField("generalUseAllowed");
      fGeneralUseAllowed.setAccessible(true);
    } catch (NoSuchFieldException e) {
      fGeneralUseAllowed = null;
      System.err.println("Failed to get field com.icl.saxon.expr.SingletonNodeSet.generalUseAllowed - incompatible Saxon version?");
      e.printStackTrace();
    }
  }

  private final Context myContext;
  private final int myFrameId;

  SaxonFrameImpl(Debugger.StyleFrame prev, Context context, StyleElement element) {
    super(prev, element);
    myContext = context;
    myFrameId = context.getBindery().getFrameId();
  }

  @Override
  public String getInstruction() {
    return myElement.getDisplayName();
  }

  @Override
  public List<Debugger.Variable> getVariables() {
    assert isValid();

    final ArrayList<Debugger.Variable> variables = new ArrayList<>();
    final Enumeration[] variableNames = myElement.getVariableNames();

    this.addVariables(myElement, variables, variableNames[0], true);
    this.addVariables(myElement, variables, variableNames[1], false);

    variables.sort(VariableComparator.INSTANCE);

    return variables;
  }

  @Override
  public Value eval(String expr) throws Debugger.EvaluationException {
    assert isValid();

    try {
      // trick to avoid exception when evaluating variable references on xsl:template frames
      final MyDummyElement dummy = new MyDummyElement(myElement);
      final Expression expression = Expression.make(expr, dummy.getStaticContext());
      return convertValue(expression.evaluate(myContext));
    } catch (XPathException e) {
      throw new Debugger.EvaluationException(e.getMessage());
    }
  }

  private Value convertValue(com.icl.saxon.expr.Value v) throws XPathException {
    return MyValue.create(v, myContext);
  }

  void addVariables(StyleElement element, ArrayList<Debugger.Variable> variables, Enumeration enumeration, boolean isGlobal) {
    final Context context = myContext;
    final StaticContext ctx = context.getStaticContext();

    final NamePool pool = element.getNamePool();
    final Bindery bindery = context.getBindery();

    while (enumeration.hasMoreElements()) {
      String name = (String)enumeration.nextElement();
      try {
        final String[] parts = name.split("\\^");
        final String realname = parts[1];
        final int fingerprint = ctx != null ? ctx.getFingerprint(realname, false) : pool.getFingerprint(parts[0], realname);
        final Binding binding = element.getVariableBinding(fingerprint);
        final Debugger.Variable.Kind kind =
          binding instanceof XSLParam ? Debugger.Variable.Kind.PARAMETER : Debugger.Variable.Kind.VARIABLE;
        final com.icl.saxon.expr.Value value = bindery.getValue(binding, myFrameId);

        if (binding instanceof XSLGeneralVariable) {
          final XSLGeneralVariable v = (XSLGeneralVariable)binding;
          final String id = v.getSystemId();
          variables.add(new VariableImpl(realname, convertValue(value), isGlobal, kind, id != null ? id.replaceAll(" ", "%20") : null,
                                         v.getLineNumber()));
        } else {
          variables.add(new VariableImpl(realname, convertValue(value), isGlobal, kind, null, -1));
        }
      } catch (XPathException e) {
        // this should never happen I guess...
        e.printStackTrace();
      }
    }
  }

  private static class MyValue implements Value {
    private final Object myValue;
    private final Type myType;

    MyValue(Object value, String type) {
      myValue = value;
      myType = new ObjectType(type);
    }

    MyValue(Object value, int type) {
      myValue = value;
      myType = mapType(type);
    }

    @Override
    public Object getValue() {
      return myValue;
    }

    @Override
    public Type getType() {
      return myType;
    }

    private static Type mapType(int type) {
      switch (type) {
        case com.icl.saxon.expr.Value.BOOLEAN:
          return XPathType.BOOLEAN;
        case com.icl.saxon.expr.Value.NODESET:
          return XPathType.NODESET;
        case com.icl.saxon.expr.Value.NUMBER:
          return XPathType.NUMBER;
        case com.icl.saxon.expr.Value.STRING:
          return XPathType.STRING;
        case com.icl.saxon.expr.Value.OBJECT:
          return XPathType.OBJECT;
        default:
          return XPathType.UNKNOWN;
      }
    }

    public static Value create(com.icl.saxon.expr.Value v, Context context) throws XPathException {
      if (v instanceof NodeSetValue) {
        if (v instanceof FragmentValue) {
          final FragmentValue value = (FragmentValue)v;
          final boolean b = value.isGeneralUseAllowed();
          try {
            if (!b) value.allowGeneralUse();

            final DocumentInfo node = value.getRootNode();
            final GeneralOutputter outputter = new GeneralOutputter(node.getNamePool());
            final StringWriter writer = new StringWriter();
            final Properties props = new Properties();
            props.setProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            outputter.setOutputDestination(props, new StreamResult(writer));
            node.copy(outputter);

            return new MyValue(writer.toString(), v.getDataType());
          } catch (TransformerException e) {
            return new MyValue(v.asString(), v.getDataType());
          } finally {
            if (!b && fGeneralUseAllowed != null) {
              resetGeneralUseAllowed(value);
            }
          }
        } else if (v instanceof TextFragmentValue) {
          // this really is just a string
          return new MyValue(v.asString(), com.icl.saxon.expr.Value.STRING);
        }

        final List<Node> list = new ArrayList<>();
        final NodeEnumeration nodeEnumeration = ((NodeSetValue)v).enumerate();
        while (nodeEnumeration.hasMoreElements()) {
          final NodeInfo node = nodeEnumeration.nextElement();
          final String path = Navigator.getPath(node);
          final String id = node.getSystemId();
          if (id != null) {
            try {
              list.add(new Node(new URI(id.replaceAll(" ", "%20")).normalize().toASCIIString(), node.getLineNumber(), path,
                                node.getStringValue()));
            } catch (URISyntaxException e) {
              debug(e);
              list.add(new Node(id, node.getLineNumber(), path, node.getStringValue()));
            }
          } else {
            list.add(new Node(null, -1, path, node.getStringValue()));
          }
        }
        return new MyValue(new NodeSet(v.asString(), list), v.getDataType());
      } else if (v instanceof ObjectValue) {
        final Object o = ((ObjectValue)v).getObject();
        return new MyValue(o, o != null ? o.getClass().getName() : "null");
      } else if (v != null) {
        return new MyValue(v.evaluateAsString(context), v.getDataType());
      } else {
        return new MyValue("", "<uninitialized>");
      }
    }
  }

  private static void resetGeneralUseAllowed(FragmentValue value) {
    try {
      fGeneralUseAllowed.set(value, false);
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  }

  private static class MyDummyElement extends StyleElement {
    private final StyleElement myElement;

    MyDummyElement(StyleElement element) {
      myElement = element;
      substituteFor(element);
    }

    @Override
    public void prepareAttributes() throws TransformerConfigurationException {
    }

    @Override
    public void process(Context context) throws TransformerException {
    }

    public StaticContext getStaticContext() {
      return new ExpressionContext(this);
    }

    @Override
    public Node getPreviousSibling() {
      return myElement.getPreviousSibling();
    }
  }
}
