/*
 * 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;

import org.intellij.plugins.xsltDebugger.rt.engine.*;
import org.intellij.plugins.xsltDebugger.rt.engine.local.saxon.SaxonSupport;
import org.intellij.plugins.xsltDebugger.rt.engine.local.saxon9.Saxon9Support;
import org.intellij.plugins.xsltDebugger.rt.engine.local.xalan.XalanSupport;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class LocalDebugger implements Debugger {
  private final Thread myThread;

  private final BreakpointManager myBreakpointManager;
  private final OutputEventQueueImpl myEventQueue;
  private volatile Condition myCurrentStopCondition;

  private final Object theLock = new Object();

  private volatile State myState = State.CREATED;

  private final LinkedList<StyleFrame> myFrames = new LinkedList<>();
  private final LinkedList<SourceFrame> mySourceFrames = new LinkedList<>();

  public LocalDebugger(final Transformer transformer, final Source source, final Result result) {
    prepareTransformer(transformer);

    myBreakpointManager = new BreakpointManagerImpl();
    myEventQueue = new OutputEventQueueImpl(this);

    myThread = new Thread(() -> {
      try {
        synchronized (theLock) {
          myState = State.RUNNING;
          theLock.notifyAll();
        }
        transformer.transform(source, result);
        stopped();
      } catch (DebuggerStoppedException e) {
        // OK
      } catch (TransformerException e) {
        // TODO: log, pass to client
        e.printStackTrace();
        stopped();
      }
    }, "local debugger");
  }

  protected void prepareTransformer(Transformer transformer) {
    try {
      if (Saxon9Support.init(transformer, this)) {
        return;
      }
    } catch (NoClassDefFoundError e1) {
      // ignore
    }
    try {
      if (SaxonSupport.init(transformer, this)) {
        return;
      }
    } catch (NoClassDefFoundError e) {
      // ignore
    }
    try {
      if (XalanSupport.init(transformer, this)) {
        return;
      }
    } catch (NoClassDefFoundError e1) {
      // ignore
    }
    throw new UnsupportedOperationException("Unsupported Transformer: " + transformer.getClass().getName());
  }

  private void suspendAndWait() throws DebuggerStoppedException {
    try {
      synchronized (theLock) {
        myCurrentStopCondition = null;

        myState = State.SUSPENDED;
        theLock.notifyAll();

        do {
          theLock.wait();
        }
        while (myState == State.SUSPENDED);

        if (myState == State.STOPPED) {
          throw new DebuggerStoppedException();
        }
      }
    } catch (InterruptedException e) {
      throw new DebuggerStoppedException();
    }
  }

  @Override
  public void resume() throws DebuggerStoppedException {
    synchronized (theLock) {
      if (myState == State.STOPPED) {
        throw new DebuggerStoppedException();
      } else if (myState != State.SUSPENDED) {
        throw new IllegalStateException();
      }

      myState = State.RUNNING;
      theLock.notifyAll();
    }
  }

  @Override
  public void pause() {
    synchronized (theLock) {
      if (myState == State.STOPPED) {
        throw new DebuggerStoppedException();
      } else if (myState != State.RUNNING) {
        throw new IllegalStateException();
      }

      myCurrentStopCondition = Condition.TRUE;
    }
  }

  public void stopped() {
    assert Thread.currentThread() == myThread;
    stop0();
  }

  private void stop0() {
    synchronized (theLock) {
      myState = State.STOPPED;
      theLock.notifyAll();
    }
  }

  @Override
  public State getState() {
    synchronized (theLock) {
      return myState;
    }
  }

  @Override
  @SuppressWarnings({ "deprecation" })
  public void stop(boolean force) {
    stop0();
    myThread.interrupt();

    if (!force) {
      return;
    }
    try {
      myThread.join(1000);
      if (myThread.isAlive()) {
        myThread.stop();
      }
    } catch (InterruptedException e) {
      //
    }
  }

  @Override
  public State waitForStateChange(State state) {
    try {
      synchronized (theLock) {
        if (myState == State.STOPPED) {
          return State.STOPPED;
        }
        while (myState == state) {
          theLock.wait();
        }

        return myState;
      }
    } catch (InterruptedException e) {
      return null;
    }
  }

  @Override
  public boolean waitForDebuggee() {
    try {
      synchronized (theLock) {
        while (myState == State.RUNNING) {
          theLock.wait();
        }

        return myState != State.STOPPED;
      }
    } catch (InterruptedException e) {
      return false;
    }
  }

  @Override
  public boolean isStopped() {
    synchronized (theLock) {
      return myState == State.STOPPED;
    }
  }

  @Override
  public boolean start() {
    assert myState == State.CREATED : "Already started";

    myThread.start();

    try {
      synchronized (theLock) {
        while (myState == State.CREATED) {
          theLock.wait();
        }
      }
      return true;
    } catch (InterruptedException e) {
      //
    }
    return false;
  }

  public void enter(StyleFrame frame) {
    myFrames.addFirst(frame);
    final String uri = frame.getURI();

    final StyleFrame previous = frame.getPrevious();
    //if (previous != null && previous.getLineNumber() == frame.getLineNumber() && uri != null && uri.equals(previous.getURI())) {
    //  if (frame.getInstruction().equals(previous.getInstruction())) {
    //    System.err.println(
    //      "WARN: Same instruction <" + frame.getInstruction() + "> on line " + frame.getLineNumber() + " encountered more than once");
    //  }
    //}

    if (isStopped()) {
      throw new DebuggerStoppedException();
    } else if (myCurrentStopCondition != null && myCurrentStopCondition.value()) {
      suspendAndWait();
    } else {
      final int lineNumber = frame.getLineNumber();
      final Breakpoint breakpoint = myBreakpointManager.getBreakpoint(uri, lineNumber);
      if (breakpoint != null && breakpoint.isEnabled()) {
        // do not evaluate a log or condition bp more than once on the same line
        if (previous == null || previous.getLineNumber() != lineNumber) {
          final String condition = breakpoint.getCondition();
          try {
            if (evalCondition(condition)) {
              final String logMessage = breakpoint.getLogMessage();
              final String traceMessage = breakpoint.getTraceMessage();

              if (logBreakpoint(frame, logMessage, traceMessage) || breakpoint.isSuspend()) {
                suspendAndWait();
              }
            }
          } catch (EvaluationException e) {
            // TODO: send to IDEA
            System.err.println("[" + lineNumber + "]: Failed to evaluate expression: " + condition + " -- " + e.getMessage());
            breakpoint.setEnabled(false);
          }
        }
      }
    }
  }

  private boolean evalCondition(String condition) throws EvaluationException {
    if (condition != null && !condition.isEmpty()) {
      if (!"true".equals(eval("boolean(" + condition + ")").getValue().toString())) {
        return false;
      }
    }
    return true;
  }

  private boolean logBreakpoint(StyleFrame frame, String logMessage, String traceMessage) throws EvaluationException {
    if (logMessage != null) {
      final String uri = frame.getURI();
      final String pos = uri.substring(uri.lastIndexOf('/') + 1) + ":" + frame.getLineNumber();
      System.out.println("[" + pos + "]: " + (!logMessage.isEmpty() ? eval(logMessage).getValue().toString() : "<no message>"));

      if (traceMessage != null) {
        myEventQueue.trace(makeTraceMessage(traceMessage));
      }
      return false;
    } else if (traceMessage != null) {
      myEventQueue.trace(makeTraceMessage(traceMessage));
      return false;
    }
    return true;
  }

  private String makeTraceMessage(String traceMessage) throws EvaluationException {
    if (!traceMessage.isEmpty()) {
      return eval(traceMessage).getValue().toString();
    } else {
      return null;
    }
  }

  public void leave() {
    if (isStopped()) {
      throw new DebuggerStoppedException();
//        } else if (myBreakpointManager.isBreakpoint(uri, lineNumber)) {
//            suspendAndWait();
//        } else if (myCurrentStopCondition != null && myCurrentStopCondition.value()) {
//            suspendAndWait();
    }

    ((AbstractFrame<?>)myFrames.removeFirst()).invalidate();
  }

  @Override
  public void step() {
    final int targetSize = myFrames.size();

    myCurrentStopCondition = new Condition() {
      @Override
      public boolean value() {
        return myFrames.size() <= targetSize;
      }
    };
    resume();
  }

  @Override
  public void stepInto() {
    myCurrentStopCondition = Condition.TRUE;

    resume();
  }

  @Override
  public Value eval(String expr) throws EvaluationException {
    final StyleFrame frame = getCurrentFrame();
    if (frame == null) {
      throw new EvaluationException("No frame available");
    }
    return frame.eval(expr);
  }

  @Override
  public StyleFrame getCurrentFrame() {
    return !myFrames.isEmpty() ? myFrames.getFirst() : null;
  }

  @Override
  public SourceFrame getSourceFrame() {
    return !mySourceFrames.isEmpty() ? mySourceFrames.getFirst() : null;
  }

  @Override
  public List<Debugger.Variable> getGlobalVariables() {
    final List<Variable> vars = getCurrentFrame().getVariables();
    for (Iterator<Variable> it = vars.iterator(); it.hasNext(); ) {
      Variable var = it.next();
      if (!var.isGlobal()) {
        it.remove();
      }
    }
    return vars;
  }

  @Override
  public BreakpointManager getBreakpointManager() {
    return myBreakpointManager;
  }

  public void pushSource(SourceFrame sourceFrame) {
    mySourceFrames.addFirst(sourceFrame);
  }

  public void popSource() {
    ((AbstractFrame<?>)mySourceFrames.removeFirst()).invalidate();
  }

  interface Condition {
    Condition TRUE = new Condition() {
      @Override
      public boolean value() {
        return true;
      }
    };

    boolean value();
  }

  @Override
  public OutputEventQueueImpl getEventQueue() {
    return myEventQueue;
  }

  @Override
  public boolean ping() {
    return true;
  }
}
