/*******************************************************************************
 * Copyright (c) 2008 ARM Limited and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * ARM Limited - Initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.debug.internal.core.model;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.cdt.core.IAddressFactory;
import org.eclipse.cdt.debug.core.CDebugCorePlugin;
import org.eclipse.cdt.debug.core.cdi.CDIException;
import org.eclipse.cdt.debug.core.cdi.event.ICDIBreakpointMovedEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDIBreakpointProblemEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDIChangedEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDICreatedEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDIDestroyedEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDIEvent;
import org.eclipse.cdt.debug.core.cdi.event.ICDIEventListener;
import org.eclipse.cdt.debug.core.cdi.model.ICDIInstruction;
import org.eclipse.cdt.debug.core.cdi.model.ICDILocationBreakpoint;
import org.eclipse.cdt.debug.core.cdi.model.ICDIMixedInstruction;
import org.eclipse.cdt.debug.core.cdi.model.ICDITarget;
import org.eclipse.cdt.debug.core.model.ICStackFrame;
import org.eclipse.cdt.debug.core.model.IDisassemblyInstruction;
import org.eclipse.cdt.debug.core.model.IDisassemblyLine;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;

public class DisassemblyRetrieval extends CDebugElement implements ICDIEventListener {

    public static final int FLAGS_SHOW_INSTRUCTIONS = 0x1;
    public static final int FLAGS_SHOW_SOURCE = 0x2;

    private Object fInput = null;
    private BigInteger fBaseElement = null;
    private int fCurrentOffset = 0;
    private int fFlags = 0;
    private IDisassemblyLine[] fLines;

    public DisassemblyRetrieval( CDebugTarget target ) {
        super( target );
        fLines = new IDisassemblyLine[0];
        CDebugCorePlugin.getDefault().getDisassemblyContextService().register( this );
        getCDISession().getEventManager().addEventListener( this );
    }

    public void dispose() {
        getCDISession().getEventManager().removeEventListener( this );
        CDebugCorePlugin.getDefault().getDisassemblyContextService().unregister( this );
    }

    /* (non-Javadoc)
     * @see org.eclipse.cdt.debug.core.cdi.event.ICDIEventListener#handleDebugEvents(org.eclipse.cdt.debug.core.cdi.event.ICDIEvent[])
     */
    public void handleDebugEvents( ICDIEvent[] events ) {
        for ( ICDIEvent event : events ) {
            Object source = event.getSource();
            if ( (event instanceof ICDICreatedEvent
                    || event instanceof ICDIChangedEvent
                    || event instanceof ICDIDestroyedEvent
                    || event instanceof ICDIBreakpointMovedEvent
                    || event instanceof ICDIBreakpointProblemEvent )
                  && source instanceof ICDILocationBreakpoint ) {
                BigInteger address = ((ICDILocationBreakpoint)source).getLocator().getAddress();
                if ( address != null ) {
                    int index = getIndexForAddress( address, fLines );
                    if ( index >= 0 ) {
                        fireEvent( new DebugEvent( fLines[index], DebugEvent.CHANGE, DebugEvent.STATE ) );
                    }
                    if ( event instanceof ICDIBreakpointMovedEvent ) {
                        address = ((ICDIBreakpointMovedEvent)event).getNewLocation().getAddress();
                        if ( address != null ) {
                            index = getIndexForAddress( address, fLines );
                            if ( index >= 0 ) {
                                fireEvent( new DebugEvent( fLines[index], DebugEvent.CHANGE, DebugEvent.STATE ) );
                            }
                        }
                    }
                }
            }
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.cdt.debug.core.model.IDisassemblyRetrieval#getInput()
     */
    public Object getInput() {
        return fInput;
    }

    public BigInteger getBaseElement() {
        return fBaseElement;
    }

    public int getCurrentOffset() {
        return fCurrentOffset;
    }

    public IDisassemblyLine[] getLines() {
        return fLines;
    }

    public void changeBase( Object input, int offset, int flags ) throws DebugException {
        if ( input instanceof ICStackFrame ) {
            fInput = input;
            ICStackFrame frame = (ICStackFrame)input;
            BigInteger address = frame.getAddress().getValue();
            if ( !containsAddress( address, fLines ) ) {
                fCurrentOffset = 0;
                reset();
            }
            else if ( flags != fFlags ) {
                reset();
            }
            else {
                fCurrentOffset += getDistance( fBaseElement, address );
            }
            fFlags = flags;
            fBaseElement = address;
        }
    }

    public void retrieveDisassembly( Object input, Object base, int offset, int lineCount, boolean reveal, int flags ) throws DebugException {
        fFlags = flags;
        boolean showInstructions = ( (flags & FLAGS_SHOW_INSTRUCTIONS) != 0 );
        boolean showSource = ( (flags & FLAGS_SHOW_SOURCE) != 0 );
        List<IDisassemblyLine> lines = new ArrayList<IDisassemblyLine>( lineCount );
        BigInteger startAddress = getCurrentStartAddress();
        BigInteger address = null;
        if ( startAddress != null ) { 
            if ( getCurrentOffset() > offset ) {
                // scrolling up
                address = startAddress.subtract( BigInteger.valueOf( getMinInstructionSize() * (getCurrentOffset() - offset) ) ); 
            }
            else if ( getCurrentOffset() < offset ) {
                // scrolling down
                IDisassemblyInstruction next = getNextInstruction( startAddress, fLines );
                if ( next != null )
                    address = next.getAdress().getValue();
            }
            else {
                address = startAddress;
            }
        }
        if ( address == null )
            address = fBaseElement;
        lines.addAll( Arrays.asList( disassembleDown( address, lineCount, showSource ) ) );
        fLines = lines.toArray( new IDisassemblyLine[lines.size()] );
        fCurrentOffset = offset;
    }

    private boolean containsAddress( BigInteger address, IDisassemblyLine[] lines ) {
        return ( getIndexForAddress( address, lines ) >= 0 );
    }

    public void reset() {
        fLines = new IDisassemblyLine[0];
    }

    private int getDistance( BigInteger address1, BigInteger address2 ) {
        int index1 = getIndexForAddress( address1, fLines );
        Assert.isTrue( index1 >=0 );
        int index2 = getIndexForAddress( address2, fLines );
        Assert.isTrue( index2 >=0 );
        return index2 - index1;
    }

    private int getIndexForAddress( BigInteger address, IDisassemblyLine[] lines ) {
        for ( int i = 0; i < lines.length; ++i ) {
            if ( lines[i] instanceof IDisassemblyInstruction && 
                 address.compareTo( ((IDisassemblyInstruction)lines[i]).getAdress().getValue() ) == 0 )
                return i;
        }
        return -1;
    }

    private BigInteger getCurrentStartAddress() {
        for ( IDisassemblyLine l : fLines ) {
            if ( l instanceof IDisassemblyInstruction )
                return ((IDisassemblyInstruction)l).getAdress().getValue();
        }
        return null;
    }
    
    private IDisassemblyLine[] disassembleDown( BigInteger address, int lineCount, boolean mixed ) throws DebugException {
        BigInteger startAddress = address;
        IDisassemblyLine[] lines = new IDisassemblyLine[0];
        BigInteger endAddress = startAddress;
        if ( lines.length < lineCount && endAddress.compareTo( getGlobalEndAddress() ) < 0 ) {
            endAddress = address.add( BigInteger.valueOf( lineCount * getMaxInstructionSize() ) );
            if ( endAddress.compareTo( getGlobalEndAddress() ) > 0 )
                endAddress = getGlobalEndAddress();
            lines = disassemble( address, endAddress, mixed );
            IDisassemblyInstruction firstInstruction = getFirstInstruction( lines );
            IDisassemblyInstruction lastInstruction = getLastInstruction( lines );
            if ( firstInstruction != null && lastInstruction != null ) {
                if ( startAddress.compareTo( firstInstruction.getAdress().getValue() ) < 0 ) {
                    lines = appendLines( disassemble( startAddress, firstInstruction.getAdress().getValue(), mixed ), lines );
                }
                startAddress = lastInstruction.getAdress().getValue();
                if ( startAddress.compareTo( endAddress ) < 0 ) {
                    IDisassemblyLine[] extraLines = new IDisassemblyLine[0];
                    while( extraLines.length == 0 && endAddress.compareTo( getGlobalEndAddress() ) < 0 ) {
                        endAddress = endAddress.add( BigInteger.valueOf( getMaxInstructionSize() ) );
                        extraLines = disassemble( startAddress, endAddress, mixed ); 
                    }
                    lines = appendLines( lines, extraLines );
                }
            }
        }
        int size = Math.min( lineCount, lines.length );
        IDisassemblyLine[] result = new IDisassemblyLine[size];
        int start = getIndexForAddress( address, lines );
        if ( start != -1 ) {
            System.arraycopy( lines, start, result, 0, size );
        }
        return result;
    }

    private IDisassemblyLine[] disassemble( BigInteger startAddress, BigInteger endAddress, boolean mixed ) throws DebugException {
        List<IDisassemblyLine> list = new ArrayList<IDisassemblyLine>();
        ICDITarget cdiTarget = (ICDITarget)getDebugTarget().getAdapter( ICDITarget.class );
        try {
            ICDIMixedInstruction[] mixedInstructions = null;
            ICDIInstruction[] asmInstructions = null;
            if ( mixed ) {
                mixedInstructions = cdiTarget.getMixedInstructions( startAddress, endAddress );
                if ( mixedInstructions.length == 0 
                     || mixedInstructions.length == 1 
                     && mixedInstructions[0].getInstructions().length == 0 ) {
                    mixedInstructions = null;
                }
            }
            if ( mixedInstructions == null ) {
                asmInstructions = cdiTarget.getInstructions( startAddress, endAddress );
            }
            if ( mixedInstructions != null ) {
                for ( ICDIMixedInstruction mi : mixedInstructions ) {
                    ICDIInstruction[] instructions = mi.getInstructions();
                    if ( instructions.length > 0 ) {
                        list.add( new DisassemblySourceLine( (CDebugTarget)getDebugTarget(), fBaseElement, mi ) );
                        for ( ICDIInstruction i : instructions ) {
                            list.add( new DisassemblyInstruction( (CDebugTarget)getDebugTarget(), fBaseElement, i ) );
                        }
                    }
                }
            }
            else if ( asmInstructions != null ) {
                for ( ICDIInstruction i : asmInstructions ) {
                    list.add( new DisassemblyInstruction( (CDebugTarget)getDebugTarget(), fBaseElement, i ) );
                }
            }
        }
        catch( CDIException exc ) {
            throw new DebugException( new Status( IStatus.ERROR, "dummy", exc.getDetailMessage(), exc ) ); //$NON-NLS-1$
        }
        return list.toArray( new IDisassemblyLine[list.size()] );
    }
    
    private int getMaxInstructionSize() {
        return 4;
    }

    private int getMinInstructionSize() {
        return 1;
    }

    private IDisassemblyInstruction getNextInstruction( BigInteger address, IDisassemblyLine[] lines ) {
        int index = getIndexForAddress( address, lines );
        if ( index == -1 || index == lines.length - 1 )
            return null;
        for ( int i = index + 1; i < lines.length; ++i ) {
            if ( lines[i] instanceof IDisassemblyInstruction )
                return (IDisassemblyInstruction)lines[i];
        }
        return null;
    }

    private IDisassemblyInstruction getPreviousInstruction( BigInteger baseAddress ) throws DebugException {
        BigInteger endAddress = baseAddress.add( BigInteger.valueOf( 1 ) );
        BigInteger startAddress = baseAddress;
        IDisassemblyLine[] lines = new IDisassemblyLine[0];
        int index = -1;
        while( index == -1 && startAddress.compareTo( getGlobalStartAddress() ) > 0 ) {
            startAddress = startAddress.subtract( BigInteger.valueOf( 1 ) );
            lines = disassemble( startAddress, endAddress, false );
            index = getIndexForAddress( baseAddress, lines );
        }
        return ( index > 0 ) ? (IDisassemblyInstruction)lines[index - 1] : null;
    }

    private IDisassemblyInstruction getFirstInstruction( IDisassemblyLine[] lines ) {
        for ( IDisassemblyLine l : lines ) {
            if ( l instanceof IDisassemblyInstruction )
                return (IDisassemblyInstruction)l;
        }
        return null;
    }

    private IDisassemblyInstruction getLastInstruction( IDisassemblyLine[] lines ) {
        for ( int i = lines.length - 1; i >= 0; --i ) {
            if ( lines[i] instanceof IDisassemblyInstruction )
                return (IDisassemblyInstruction)lines[i];
        }
        return null;
    }

    private BigInteger getGlobalStartAddress() {
        return getAddressFactory().getZero().getValue();
    }

    private BigInteger getGlobalEndAddress() {
        return getAddressFactory().getMax().getValue();
    }

    private IAddressFactory getAddressFactory() {
        return ((CDebugTarget)getDebugTarget()).getAddressFactory();
    }

    private IDisassemblyLine[] appendLines( IDisassemblyLine[] lines1, IDisassemblyLine[] lines2 ) {
        List<IDisassemblyLine> list = new ArrayList<IDisassemblyLine>( lines1.length + lines2.length );
        list.addAll( Arrays.asList( lines1 ) );
        for ( IDisassemblyLine l : lines2 ) {
            if ( !list.contains( l ) )
                list.add( l );
        }
        return list.toArray( new IDisassemblyLine[list.size()] );
    }
}
