/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * 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.drools.eclipse.debug.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.drools.eclipse.DRLInfo.RuleInfo;
import org.drools.eclipse.DroolsEclipsePlugin;
import org.drools.eclipse.debug.DebugUtil;
import org.drools.eclipse.debug.VariableWrapper;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IValue;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdi.internal.ObjectReferenceImpl;
import org.eclipse.jdt.debug.core.IJavaClassObject;
import org.eclipse.jdt.debug.core.IJavaFieldVariable;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.internal.debug.core.model.JDILocalVariable;
import org.eclipse.jdt.internal.debug.core.model.JDIObjectValue;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.ArrayReference;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.IntegerValue;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.InvocationException;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.StringReference;
import com.sun.jdi.Value;

/**
 * Stack Frame for MVEL Dialect
 */
public class MVELStackFrame extends DroolsStackFrame {

    private final MVELStackFrameContext ctxCache = new MVELStackFrameContext();

    public MVELStackFrame(DroolsThread thread,
                          StackFrame frame,
                          int depth) {
        super( thread,
               frame,
               depth );

    }

    @Override
    public RuleInfo getExecutingRuleInfo() {
        try {
            String type = getDeclaringTypeName();

            return DroolsEclipsePlugin.getDefault().getRuleInfoByClass( type );

        } catch ( DebugException exc ) {
            DroolsEclipsePlugin.log( exc );
        }
        return null;
    }

    @Override
    public IVariable[] getVariables() throws DebugException {
        synchronized ( getThread() ) {
            if ( !isSuspended() ) {
                return new IVariable[0];
            }

            IVariable[] cache = ctxCache.getCacheVariables();
            if ( cache != null ) {
                return cache;
            }

            List<IVariable> result = new ArrayList<IVariable>( 0 );

            Method method = getUnderlyingMethod(); // onBreak
            ReferenceType declaringType = method.declaringType(); // org.drools.core.base.mvel.MVELDebugHandler

            try {
                Object var = method.variables().get( 0 );
                LocalVariable v2 = (LocalVariable) var;
                JDILocalVariable frameLocal = new JDILocalVariable( this,
                                                                    v2 );

                IValue knownVars = DebugUtil.getValueByExpression( "return getFactory().getKnownVariables().toArray(new String[0]);",
                                                                   frameLocal.getValue() );

                JDIObjectValue vvv = (JDIObjectValue) knownVars;

                if ( vvv != null && ((ArrayReference) vvv.getUnderlyingObject()).length() > 0 ) {
                    ArrayReference arr = (ArrayReference) vvv.getUnderlyingObject();

                    Iterator<Value> varIter = arr.getValues().iterator();

                    while ( varIter.hasNext() ) {
                        final String varName = ((StringReference) varIter.next()).value();

                        IJavaValue val = (IJavaValue) DebugUtil.getValueByExpression( "return getFactory().getVariableResolver(\"" + varName + "\").getValue();",
                                                                                      frameLocal.getValue() );
                        if ( val != null ) {
                            final ObjectReference valRef = ((JDIObjectValue) val).getUnderlyingObject();
                            VariableWrapper local = new VariableWrapper( varName,
                                                                         val );

                            local.setPublic( true );
                            result.add( local );
                        } else {
                            DroolsEclipsePlugin.log( new Exception( "Unable to get value for variable named '" + varName + "' suspend=" + isSuspended() ) );
                        }
                    }

                }

                IVariable[] vararr = result.toArray( new IVariable[result.size()] );
                Arrays.sort( vararr,
                             new Comparator<IVariable>() {
                                 public int compare(IVariable var1,
                                                    IVariable var2) {
                                     try {
                                         return var1.getName().compareTo( var2.getName() );
                                     } catch ( DebugException e ) {
                                         return 0;
                                     }
                                 }
                             } );
                result = new ArrayList<IVariable>( (vararr.length - 1) / 2 );
                for ( int i = 0; i < vararr.length; i++ ) {
                    String name = vararr[i].getName();
                    if ( !(name.equals( DROOLS_VAR_NAME )) && !(name.endsWith( HANDLE_SUFIX )) ) {
                        result.add( vararr[i] );
                    }
                }
                vararr = result.toArray( new IVariable[result.size()] );
                
                ctxCache.setCacheVariables( vararr );
                return vararr;

            } catch ( Throwable t ) {
                DroolsEclipsePlugin.log( t );
            }
            return new IVariable[0];
        }
    }
    public int getLineNumber() throws DebugException {
        synchronized ( getThread() ) {
            int cache = ctxCache.getCacheLineNumber();
            if ( cache != -1 ) {
                return cache;
            }

            DroolsDebugTarget t = (DroolsDebugTarget) getDebugTarget();
            String sourceName = getMVELName();
            DroolsLineBreakpoint bpoint = (DroolsLineBreakpoint) t.getDroolsBreakpoint( sourceName );
            if ( bpoint == null ) {
                return -1;
            }

            int line;
            try {
                line = Integer.parseInt( bpoint.getFileRuleMappings().get( sourceName ).toString() );
            } catch ( Throwable t2 ) {
                DroolsEclipsePlugin.log( t2 );
                return -1;
            }

            int fragmentLine = getBreakpointLineNumber(); // 4->5 for step over
            int res = line + fragmentLine;

            if ( fragmentLine == -1 ) {
                return -1;
            }
            ctxCache.setCacheLineNumber( res );
            return res;
        }
    }

    private int getBreakpointLineNumber() {
        if ( ctxCache.getCacheBreakpointLineNumber() != -1 ) {
            return ctxCache.getCacheBreakpointLineNumber();
        }

        // Drools 4
        try {
            Object o = getRemoteVar( "lineNumber" );
            if ( o == null ) {
                return -1;
            }
            IntegerValue val = (IntegerValue) o;
            int realval = val.value();
            ctxCache.setCacheBreakpointLineNumber( realval );
            return realval;
        } catch ( NullPointerException e ) {
            // Drools 5+
        } catch ( Throwable e ) {
            DroolsEclipsePlugin.log( e );
        }

        // Drools 5
        try {
            Object o = getRemoteVar( "label" );
            if ( o == null ) {
                return -1;
            }
            ObjectReference obj = (ObjectReference) o;
            ClassType frameType = (ClassType) obj.type();
            Field field = frameType.fieldByName( "lineNumber" );
            o = obj.getValue( field );
            if ( o == null ) {
                return -1;
            }
            IntegerValue val = (IntegerValue) o;
            int realval = val.value();
            ctxCache.setCacheBreakpointLineNumber( realval );
            return realval;
        } catch ( NullPointerException e ) {
            // Drools 5+
        } catch ( Throwable e ) {
            DroolsEclipsePlugin.log( e );
        }

        return -1;
    }

    public String getMVELName() {
        synchronized ( getThread() ) {
            String cache = ctxCache.getCacheMVELName();
            if ( cache != null ) {
                return cache;
            }
            
            if ( !isSuspended() ) {
                return null;
            }

            // Drools 4
            try {
                Object rem = getRemoteVar( "sourceName" );
                if ( rem == null ) {
                    return null;
                }
                StringReference res = (StringReference) rem;
                String realres = res.value();
                ctxCache.setCacheMVELName( realres );
                return realres;
            } catch ( NullPointerException e ) {
                // Drools 5
            } catch ( Throwable e ) {
                DroolsEclipsePlugin.log( e );
            }

            // Drools 5
            try {
                Object rem = getRemoteVar( "label" );
                if ( rem == null ) {
                    return null;
                }
                ObjectReference obj = (ObjectReference) rem;
                ClassType frameType = (ClassType) obj.type();
                Field field = frameType.fieldByName( "sourceFile" );
                rem = obj.getValue( field );
                if ( rem == null ) {
                    return null;
                }
                StringReference res = (StringReference) rem;
                String realres = res.value();
                ctxCache.setCacheMVELName( realres );
                return realres;
            } catch ( Throwable e ) {
                DroolsEclipsePlugin.log( e );
            }

            return "Unavailable";
        }
    }

    private void evalEnd() {
        fireChangeEvent( DebugEvent.STATE );
        //fireChangeEvent( DebugEvent.CONTENT );
    }

    private Object getRemoteVar(String methodName) throws AbsentInformationException,
                                                  ClassNotLoadedException,
                                                  DebugException,
                                                  InvalidTypeException,
                                                  IncompatibleThreadStateException,
                                                  InvocationException {

        //frame arg
        Method method = getUnderlyingMethod(); // onBreak
        //ReferenceType declaringType = method.declaringType(); // org.drools.core.base.mvel.MVELDebugHandler

        LocalVariable var = (LocalVariable) method.variables().get( 0 );//frame

        ClassType frameType = (ClassType) var.type();

        StackFrame frame = getUnderlyingStackFrame();
        Value value = frame.getValue( var );
        //getThread().getTopStackFrame().get

        //IValue value = jdivar.getValue();
        ObjectReferenceImpl o = (ObjectReferenceImpl) value;

        //if ( value instanceof JDINullValue ) {
        // return null;
        // }

        //ObjectReference o = (ObjectReference) ((JDIObjectValue) value).getUnderlyingObject();
        if ( o == null ) {
            return null;
        }

        Field field = frameType.fieldByName( methodName );
        Value val = o.getValue( field );
        return val;
    }

    public String getSourcePath() throws DebugException {
        return getMVELName();
    }

    public String getSourceName() throws DebugException {
        return getMVELName();
    }
    
    public void stepOver() throws DebugException {
        synchronized ( getThread() ) {
            ctxCache.clear();
        }
        super.stepOver();
    }

    public boolean canStepInto() {
        return false;
    }

    public boolean canDropToFrame() {
        return false;
    }

    public boolean canStepReturn() {
        return false;
    }

    public boolean canStepWithFilters() {
        return false;
    }

    public IJavaReferenceType getReferenceType() throws DebugException {
        return REF_TYPE;
    }

    public static class MVELStackFrameContext {
        private int         cacheLineNumber           = -1;
        private int         cacheBreakpointLineNumber = -1;
        private String      cacheMVELName             = null;
        private IVariable[] cacheVariables            = null;

        public synchronized void clear() {
            cacheLineNumber = -1;
            cacheBreakpointLineNumber = -1;
            cacheMVELName = null;
            cacheVariables = null;
        }

        public synchronized int getCacheLineNumber() {
            return cacheLineNumber;
        }

        public synchronized void setCacheLineNumber(int cacheLineNumber) {
            this.cacheLineNumber = cacheLineNumber;
        }

        public synchronized int getCacheBreakpointLineNumber() {
            return cacheBreakpointLineNumber;
        }

        public synchronized void setCacheBreakpointLineNumber(int cacheBreakpointLineNumber) {
            this.cacheBreakpointLineNumber = cacheBreakpointLineNumber;
        }

        public synchronized String getCacheMVELName() {
            return cacheMVELName;
        }

        public synchronized void setCacheMVELName(String cacheMVELName) {
            this.cacheMVELName = cacheMVELName;
        }

        public synchronized IVariable[] getCacheVariables() {
            return cacheVariables;
        }

        public synchronized void setCacheVariables(IVariable[] cacheVariables) {
            this.cacheVariables = cacheVariables;
        }
    }

    /**
      * Dummy type with changed stratum to force debugger's LaunchView to show proper stackframe name
      */
    private static final IJavaReferenceType REF_TYPE = new IJavaReferenceType() {

                                                         public IJavaFieldVariable getField(String name) throws DebugException {
                                                             return null;
                                                         }

                                                         public IJavaClassObject getClassObject() throws DebugException {
                                                             return null;
                                                         }

                                                         public String[] getAvailableStrata() throws DebugException {
                                                             return null;
                                                         }

                                                         public String getDefaultStratum() throws DebugException {
                                                             return "MVEL";
                                                         }

                                                         public String[] getDeclaredFieldNames() throws DebugException {
                                                             return null;
                                                         }

                                                         public String[] getAllFieldNames() throws DebugException {
                                                             return null;
                                                         }

                                                         public IJavaObject getClassLoaderObject() throws DebugException {
                                                             return null;
                                                         }

                                                         public String getGenericSignature() throws DebugException {
                                                             return null;
                                                         }

                                                         public String getSourceName() throws DebugException {
                                                             return null;
                                                         }

                                                         public String[] getSourceNames(String stratum) throws DebugException {
                                                             return null;
                                                         }

                                                         public String[] getSourcePaths(String stratum) throws DebugException {
                                                             return null;
                                                         }

                                                         public IJavaObject[] getInstances(long max) throws DebugException {
                                                             return null;
                                                         }

                                                         public String getName() throws DebugException {
                                                             return null;
                                                         }

                                                         public String getSignature() throws DebugException {
                                                             return null;
                                                         }

                                                         public IDebugTarget getDebugTarget() {
                                                             return null;
                                                         }

                                                         public ILaunch getLaunch() {
                                                             return null;
                                                         }

                                                         public String getModelIdentifier() {
                                                             return null;
                                                         }

                                                         public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
                                                             return null;
                                                         }

                                                        public long getInstanceCount()
                                                                throws DebugException {
                                                            return 0;
                                                        }

                                                     };

}
