/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2021 University of Lausanne, Switzerland
 * https://ips-labs.unil.ch/pianos
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */

// ----------------------------
// Generic actions
// ----------------------------
/**
 * @constructor
 */

function Pianos4Action_Batch( displayName )
{
    var actions = [];
    var selectionNeedsUpdate = false;
    
    this.append = function( action )
    {
        actions.push( action );
        assert( !( 'inBatch' in action ) );
        action.inBatch = this;
        return this;
    }

    this.getName = function()
    {
        return 'batch';
    }

    this.getLabel = function()
    {
        if( displayName )
            return displayName;
        
        var d = {};
        var s = '';
        for( var i = 0; i < actions.length; ++i )
        {
            var l = actions[ i ].getLabel();
            if( l in d )
                continue;
            if( s.length )
                s += ', ';
            s += l;
            d[ l ] = true;
        }
        if( s.length )
            return s;
        else
            return 'Empty batch action';
    }

    this.redo = function()
    {
        preRun( 'redo' );
        
        for( var i = 0; i < actions.length; ++i )
            actions[ i ].redo();
        
        postRun( 'redo' );
    }

    this.undo = function()
    {
        preRun( 'undo' );
        
        for( var i = 0; i < actions.length; ++i )
            actions[ actions.length - i - 1 ].undo();
        
        postRun( 'undo' );
    }

    this.setSelectionNeedsUpdate = function()
    {
        selectionNeedsUpdate = true;
    }

    var preRun = function( mode )
    {
    }

    var postRun = function( mode )
    {
        if( selectionNeedsUpdate )
        {
            selectionNeedsUpdate = false;
            pianos.onSelectionChanged( true );
        }
    }
}

/**
 * @constructor
 */
function Pianos4Action_Custom( redoFunc, undoFunc )
{
    assert( redoFunc || undoFunc );
    
    this.getName = function()
    {
        return 'custom';
    }

    this.getLabel = function()
    {
        return 'Custom action';
    }

    this.redo = function()
    {
        if( redoFunc )
            redoFunc( this );
    }

    this.undo = function()
    {
        if( undoFunc )
            undoFunc( this );
    }
}

// ----------------------------
// Annotations (common)
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_CreateAnnotation( view, type, annotation )
{
    assert( view && annotation );
    assert( type == 'minutia' || type == 'ridge' || type == 'area' || type == 'comment' || type == 'GC' );
    
    this.getName = function()
    {
        return 'createannotation';
    }

    this.getLabel = function()
    {
        return 'Create ' + type;
    }

    this.redo = function()
    {
        switch( type )
        {
            case 'minutia':
                view.addMinutia( annotation );
                break;
            case 'ridge':
                view.addRidge( annotation );
                break;
            case 'area':
                view.addArea( annotation );
                break;
            case 'comment':
                view.addComment( annotation );
                break;
            case 'GC':
                view.add_GhostCursor_Anchor( annotation );
                break;
            default:
                assert( false );
                break;
        }
    }

    this.undo = function()
    {
        // We must de-select the annotation if it is selected. However, we delay
        // the
        // screen update of the global selection if we are in a batch. The batch
        // itself
        // will do the updating
        if( view.isSelected( annotation ) )
        {
            if( 'inBatch' in this && this.inBatch )
            {
                view.unselectAnnotation( annotation.id, true );
                this.inBatch.setSelectionNeedsUpdate();
            } else
                view.unselectAnnotation( annotation.id );
        }
        
        switch( type )
        {
            case 'minutia':
                view.removeMinutia( annotation );
                break;
            case 'ridge':
                view.removeRidge( annotation );
                break;
            case 'area':
                view.removeArea( annotation );
                break;
            case 'comment':
                view.removeComment( annotation );
                break;
            case 'GC':
                view.remove_GhostCursor_Anchor( annotation );
                break;
            default:
                assert( false );
                break;
        }
    }
}

/**
 * @constructor
 */

function Pianos4Action_ModifyAnnotation( view, annotation, newValues )
{
    assert( view && annotation );
    
    this.getName = function()
    {
        return 'modannot';
    }

    this.getLabel = function()
    {
        return label ? label : 'Modify annotation'
    }
    this.setLabel = function( lbl )
    {
        label = lbl;
    }

    this.redo = function()
    {
        annotation.modified = new Date();
        view.updateAnnotation( annotation, newValues );
    }

    this.undo = function()
    {
        annotation.modified = currentModTime;
        view.updateAnnotation( annotation, oldValues );
    }

    var oldValues = {};
    var label = null;
    var currentModTime = null;
    
    ( function()
    {
        for( var key in newValues )
        {
            // Those fields are not mutable through this functor (pairing is
            // actually a
            // mutable field but one should use the dedicated action)
            assert( key != 'id' && key != 'pairing' && key != 'created' && key != 'modified' );
            oldValues[ key ] = annotation[ key ];
        }
        assert( 'modified' in annotation && typeof ( annotation.modified ) == 'object' );
        currentModTime = annotation.modified;
    } )();
}

/**
 * @constructor
 */

function Pianos4Action_DeleteAnnotation( view, id )
{
    assert( view && id );
    
    this.getName = function()
    {
        return 'deleteannotation'
    };
    this.getLabel = function()
    {
        return 'Delete annotation'
    };
    
    this.redo = function()
    {
        if( unpair )
            unpair.redo();
        
        // We must de-select the annotation if it is selected. However, we delay
        // the
        // screen update of the global selection if we are in a batch. The batch
        // itself
        // will do the updating
        if( view.isSelected( annotation ) )
        {
            if( 'inBatch' in this && this.inBatch )
            {
                view.unselectAnnotation( id, true );
                this.inBatch.setSelectionNeedsUpdate();
            } else
                view.unselectAnnotation( id );
        }
        
        switch( type )
        {
            case 'minutia':
                view.removeMinutia( annotation );
                break;
            case 'ridge':
                view.removeRidge( annotation );
                break;
            case 'area':
                view.removeArea( annotation );
                break;
            case 'comment':
                view.removeComment( annotation );
                break;
            default:
                assert( false );
                break;
        }
    }

    this.undo = function()
    {
        switch( type )
        {
            case 'minutia':
                view.addMinutia( annotation );
                break;
            case 'ridge':
                view.addRidge( annotation );
                break;
            case 'area':
                view.addArea( annotation );
                break;
            case 'comment':
                view.addComment( annotation );
                break;
            default:
                assert( false );
                break;
        }
        
        if( unpair )
            unpair.undo();
    }

    var ret = view.getAnnotationById( id );
    var type = ret[ 0 ];
    var annotation = ret[ 1 ];
    
    var unpair = null;
    if( type == 'minutia' && 'pairing' in annotation )
    {
        var mark;
        var print;
        if( view.getType() == 'mark' )
        {
            mark = annotation;
            print = pianos.getView( 'print' ).getPairedMinutia( annotation.pairing );
            // assert( print );
        } else
        {
            mark = pianos.getView( 'mark' ).getPairedMinutia( annotation.pairing );
            // assert( mark );
            print = annotation;
        }
        
        unpair = new Pianos4Action_PairMinutiae( mark, print, null );
    }
}

function Pianos4Action_GroupAnnotations( view, annotations, groupID )
{
    assert( view && annotations && typeof ( annotations.length ) == 'number' && groupID );
    
    this.getName = function()
    {
        return 'groupannotations'
    };
    this.getLabel = function()
    {
        return 'Group annotations'
    };
    
    this.redo = function()
    {
        for( var i in annotations )
        {
            var annot = annotations[ i ];
            annot.group = -1;
            view.updateAnnotation( annot, {
                group: groupID
            } );
        }
    }

    this.undo = function()
    {
        for( var i in annotations )
        {
            var annot = annotations[ i ];
            if( annot.id in previous )
            {
                annot.group = -2;
                view.updateAnnotation( annot, {
                    group: previous[ annot.id ]
                } );
            } else
            {
                delete annot.group;
                view.updateAnnotation( annot, {} );
            }
        }
    }

    var previous = {};
    for( var i in annotations )
    {
        var annot = annotations[ i ];
        if( 'group' in annot )
            previous[ annot.id ] = annot.group;
    }
}

// ----------------------------
// Minutia-specific
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_PairMinutiae( mark, print, id )
{
    assert( mark || print );
    
    this.getName = function()
    {
        return 'pairminutiae';
    }

    this.getLabel = function()
    {
        return !mark || !print ? 'Unpair minutiae' : 'Pair minutiae';
    }

    this.redo = function()
    {
        begin();
        for( var i in ops )
            apply( ops[ i ], false );
        end();
    }

    this.undo = function()
    {
        begin();
        for( var i in ops )
            apply( ops[ i ], true );
        end();
    }

    var ops = [];
    var updates = null;
    
    var plan = function( minutia, isMark )
    {
        var previous = null;
        if( 'pairing' in minutia )
        {
            previous = minutia.pairing;
            
            var otherViewName = isMark ? 'print' : 'mark';
            var otherView = pianos.getView( otherViewName );
            
            var paired = otherView.getPairedMinutia( minutia.pairing );
            if( paired && paired.pairing == minutia.pairing )
            {
	            ops.push( {
	                view: otherView,
	                minutia: paired,
	                redo: null,
	                undo: paired.pairing
	            } );
            }
        }
        
        var viewName = isMark ? 'mark' : 'print';
        var view = pianos.getView( viewName );
        ops.push( {
            view: view,
            minutia: minutia,
            redo: id,
            undo: previous
        } );
    }

    var begin = function()
    {
        assert( !updates );
        updates = {};
    }

    var end = function()
    {
        for( var i in updates )
            updates[ i ][ 0 ].updateMinutiaPairing( updates[ i ][ 1 ] );
        updates = null;
    }

    var apply = function( op, isUndo )
    {
        var value = isUndo ? op.undo : op.redo;
        if( value !== null )
            op.minutia.pairing = value;
        else
            delete op.minutia.pairing;
        
        var updateIndex = op.view.getType() + '_' + op.minutia.id;
        if( !( updateIndex in updates ) )
            updates[ updateIndex ] = [
                op.view, op.minutia
            ];
    }

    if( mark && print && 'pairing' in mark && 'pairing' in print && mark.pairing == print.pairing )
    {
        // Re-pairing of already paired minutiae must be processed differently
        ops.push( {
            view: pianos.getView( 'mark' ),
            minutia: mark,
            redo: id,
            undo: mark.pairing
        } );
        ops.push( {
            view: pianos.getView( 'print' ),
            minutia: print,
            redo: id,
            undo: print.pairing
        } );
    } else
    {
        // All other cases use the standard algorithm
        if( mark )
            plan( mark, true );
        if( print )
            plan( print, false );
    }
}

// ----------------------------
// EditImage
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_EditImage( view, filters )
{
    assert( view && filters && typeof ( filters.length ) != 'undefined' );
    
    var previous = view.getImageFilters();
    
    this.getName = function()
    {
        return 'editimage';
    }

    this.getLabel = function()
    {
        return 'Edit image';
    }

    this.redo = function()
    {
        view.setImageFilters( filters );
    }

    this.undo = function()
    {
        view.setImageFilters( previous );
    }
}

// ----------------------------
// Conclusions/observations
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_UpdateConclusions( previous, current, runFirstTime )
{
    assert( previous && current );
    
    var firstTime = typeof ( runFirstTime ) == 'boolean' ? !runFirstTime : true;
    
    this.getName = function()
    {
        return 'modconcl';
    }

    this.getLabel = function()
    {
        return 'Conclusions changed';
    }

    this.redo = function()
    {
        if( firstTime )
        {
            firstTime = false;
            return;
        }
        pianos.setConclusions( current );
    }

    this.undo = function()
    {
        pianos.setConclusions( previous );
    }
}

// ----------------------------
// SetData
// ----------------------------

/**
 * @constructor
 */
function Pianos4Action_SetData( data, v3 )
{
    this.getName = function()
    {
        return 'setdata';
    }

    this.getLabel = function()
    {
        return data ? 'Load' : 'Clear'
    }

    this.redo = function()
    {
        if( v3 )
            pianos.setDataV3( data );
        else
            pianos.setData( data );
    }

    this.undo = function()
    {
        pianos.setData( previous );
    }

    var previous = pianos.getData();
}
