package uk.ac.starlink.ttools.cea;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.xml.sax.SAXException;
import uk.ac.starlink.task.Task;
import uk.ac.starlink.ttools.Formatter;
import uk.ac.starlink.ttools.Stilts;
import uk.ac.starlink.ttools.task.MapperTask;
import uk.ac.starlink.ttools.task.VariableTablesInput;
import uk.ac.starlink.util.LoadException;
import uk.ac.starlink.util.ObjectFactory;
import uk.ac.starlink.util.XmlWriter;

/**
 * Writes an application description file suitable for use with
 * the AstroGrid Common Execution Architecture.
 *
 * <p>There are variants of the format according to what the purpose 
 * of the output XML will be.  This class is an abstract superclass 
 * providing common methods whose concrete subclasses do the actual writing.
 *
 * <p>This makes a best effort at providing a CEA-friendly interface to
 * the capabilities of STILTS.  Call the {@link #main} method with
 * the <code>-help</code> flag for a usage message.
 *
 * @author   Mark Taylor
 * @since    17 March 2006
 */
public abstract class CeaWriter extends XmlWriter {

    private final CeaConfig config_;
    private final CeaTask[] tasks_;
    private final Formatter formatter_;
    private final FlagDef[] flagDefs_;
    private final String cmdline_;

    /**
     * Constructor.
     *
     * @param  out  output stream for XML
     * @param  config  configuration object for the specific flavour of output
     * @param  tasks  list of tasks to be described by the output
     * @param  redirects  true iff you want stdout/stderr parameters for 
     *                    standard output/error redirection
     * @param  cmdline  command line string, used for logging within the
     *                  output only
     */
    protected CeaWriter( PrintStream out, CeaConfig config, CeaTask[] tasks,
                         boolean redirects, String cmdline ) {
        super( out );
        config_ = config;
        tasks_ = tasks;
        cmdline_ = cmdline;
        formatter_ = new Formatter();
        formatter_.setManualName( "the manual" );
        flagDefs_ = redirects
            ? new FlagDef[] {
                  new RedirectFlagDef( "stdout", "File for output from task",
                                       "stilts.out" ),
                  new RedirectFlagDef( "stderr", "File for errors from task",
                                       "stilts.err" ),
              }
            : new FlagDef[ 0 ];
    }

    /**
     * Hook for additional configuration of concrete subclasses using 
     * command-line flags.
     *
     * @param  args     array of command-line arguments all of which are
     *                  directed at this object (any generic ones will have
     *                  been removed)
     * @return   0 for success, otherwise an error status
     */
    public abstract int configure( String[] args );

    /**
     * Writes the configuration XML document.
     */
    public void writeDocument() throws SAXException {
        writeDeclaration();
        println( "<!-- Automatically generated by "
             + "\n !      " + cmdline_
             + "\n !   STILTS version " + Stilts.getVersion()
             + "\n !   " + new Date()
             + "\n !-->" );
        int startLevel = getLevel();
        writeContent();
        assert getLevel() == startLevel : "Mismatched levels";
    }

    /**
     * Performs the implementation-specific output of XML elements.
     * Invoked by {@link #writeDocument}; presumably invokes
     * {@link #writeParameters} and {@link #writeInterfaces}.
     */
    protected abstract void writeContent() throws SAXException;

    /**
     * Returns the URL of the schema to which the output of this 
     * object conforms.
     *
     * @return   validation schema location
     */
    public abstract String getSchemaLocation();

    /**
     * Writes a Parameters element suitable for use with CEA.
     */
    protected void writeParameters() throws SAXException {
        startElement( config_.getParametersElement() );

        /* Write parameters which are common to all tasks. */
        for ( int i = 0; i < flagDefs_.length; i++ ) {
            flagDefs_[ i ].writeParameter();
        }

        /* Write all the parameters for all tasks.  They are given
         * reference names qualified by their task names so that they can
         * be referenced separately in their respective interfaces later. */
        for ( int i = 0; i < tasks_.length; i++ ) {
            writeParameters( tasks_[ i ] );
        }
        endElement( config_.getParametersElement() );
    }

    /**
     * Writes an Interfaces element suitable for use with CEA.
     */
    protected void writeInterfaces() throws SAXException {
        startElement( config_.getInterfacesElement() );
        for ( int i = 0; i < tasks_.length; i++ ) {
            writeInterface( tasks_[ i ] );
        }
        endElement( config_.getInterfacesElement() );
    }

    /**
     * Writes the Parameter elements for each parameter belonging to a
     * given Task.
     *
     * @param  task  task to write parameters for
     */
    private void writeParameters( CeaTask task ) throws SAXException {
        CeaParameter[] params = task.getParameters();

        /* Work out specifics of attributes permitted on the parameter 
         * element. */
        ElementDeclaration paramDecl = config_.getParameterElement();
        boolean hasFileRef = paramDecl.hasAttribute( "fileRef" );
        boolean hasSwitchType = paramDecl.hasAttribute( "switchType" );
        boolean hasCommandSwitch = paramDecl.hasAttribute( "commandSwitch" );
        boolean hasCommandPosition =
            paramDecl.hasAttribute( "commandPosition" );

        /* Write a CEA parameter definition representing the task itself.
         * This is identified only by its position on the command line,
         * and is only allowed to assume a single, fixed value. */
        StringBuffer aBuf = new StringBuffer()
            .append( formatAttribute( "name", getParamRef( task, null ) ) )
            .append( formatAttribute( "type", "text" ) );
        if ( hasFileRef ) {
            aBuf.append( formatAttribute( "fileRef", "false" ) );
        }
        if ( hasCommandPosition ) {
            aBuf.append( formatAttribute( "commandPosition",
                                          Integer
                                         .toString( flagDefs_.length + 1 ) ) );
        }
        startElement( paramDecl, aBuf.toString() );
        addElement( "UI_Name", "", "task" );
        addElement( "UI_Description", "",
                    xmlToCdata( task.getName() + ": " + task.getPurpose() ) );
        addElement( "DefaultValue", "", xmlToCdata( task.getName() ) );
        startElement( "OptionList" );
        addElement( "OptionVal", "", xmlToCdata( task.getName() ) );
        endElement( "OptionList" );
        endElement( paramDecl );

        /* Write CEA parameter definitions for each of the task's parameters. */
        for ( int iParam = 0; iParam < params.length; iParam++ ) {
            CeaParameter param = params[ iParam ];
            StringBuffer attBuf = new StringBuffer()
                .append( formatAttribute( "name", getParamRef( task, param ) ) )
                .append( formatAttribute( "type", param.getType() ) );
            if ( hasFileRef ) {
                attBuf.append( formatAttribute( "fileRef",
                                                param.isRef() ? "true"
                                                              : "false" ) );
            }
            if ( hasSwitchType ) {
                attBuf.append( formatAttribute( "switchType", "keyword" ) );
            }
            if ( hasCommandSwitch ) {
                attBuf.append( formatAttribute( "commandSwitch",
                                                param.getName() ) );
            }
            startElement( paramDecl, attBuf.toString() );
            addElement( "UI_Name", "",
                        xmlToCdata( param.getName() + " - "
                                  + param.getSummary() ) );

            /* Note that although the CEA v1 schema documentation gives
             * UI_Description content as type "agpd:xhtmlDocumentation",
             * at time of writing, it only treats it as plain text,
             * meaning that tags come out as angle brackets etc.
             * So translate to plain text here. */
            addElement( "UI_Description", "",
                        xmlToCdata( param.getDescription() ) );
            String dflt = param.getDefault();
            if ( dflt != null && dflt.trim().length() > 0 ) {
                addElement( "DefaultValue", "", dflt );
            }
            String[] options = param.getOptions();
            if ( options != null && options.length > 0 ) {
                startElement( "OptionList" );
                for ( int iOpt = 0; iOpt < options.length; iOpt++ ) {
                    addElement( "OptionVal", "", options[ iOpt ] );
                }
                endElement( "OptionList" );
            }
            endElement( paramDecl );
        }
    }

    /**
     * Writes the Interface element used for invoking a given task.
     *
     * @param   task  task
     */
    private void writeInterface( CeaTask task ) {

        /* Intro for an interface representing a STILTS task. */
        CeaParameter[] params = task.getParameters();
        startElement( "Interface", formatAttribute( "name", task.getName() ) );
        startElement( "input" );

        /* Write CEA parameter references for the standard flags. */
        for ( int iFlag = 0; iFlag < flagDefs_.length; iFlag++ ) {
            addElement( "pref",
                        formatAttribute( "ref", flagDefs_[ iFlag ].getName() ),
                        "" );
        }

        /* Write a CEA parameter reference for the parameter representing
         * the task itself. */
        addElement( "pref",
                    formatAttribute( "ref", getParamRef( task, null ) ),
                    "" );

        /* Write CEA parameter references for each of the non-output
         * parameters used by this task. */
        for ( int iParam = 0; iParam < params.length; iParam++ ) {
            CeaParameter param = params[ iParam ];
            if ( ! param.isOutput() ) {
                String atts = formatAttribute( "ref",
                                               getParamRef( task, param ) );
                if ( param.isMulti() ) {
                    atts = atts + formatAttribute( "minoccurs", "0" )
                                + formatAttribute( "maxoccurs", "0" );
                }
                else if ( param.isNullPermitted() ) {
                    atts = atts + formatAttribute( "minoccurs", "0" );
                }
                addElement( "pref", atts, "" );
            }
        }
        endElement( "input" );

        /* Write CEA parameter references for each of the output parameters
         * used by this task. */
        startElement( "output" );
        for ( int iParam = 0; iParam < params.length; iParam++ ) {
            CeaParameter param = params[ iParam ];
            if ( param.isOutput() ) {
                String atts = formatAttribute( "ref",
                                               getParamRef( task, param ) );
                addElement( "pref", atts, "" );
            }
        }
        endElement( "output" );

        /* Outro. */
        endElement( "Interface" );
    }

    /**
     * Convenience method to start an element described by 
     * an ElementDeclaration.  No attributes are written.
     *
     * @param  el  element
     */
    protected void startElement( ElementDeclaration el ) {
        startElement( el.getElementName(), el.getElementAttributes() );
    }

    /**
     * Convenience method to start an element described by an
     * ElementDeclaration with supplied attributes.
     *
     * @param  el  element
     * @param  moreAtts  formatted attribute string additional to any 
     *         defined by the element declaration
     */
    protected void startElement( ElementDeclaration el, String moreAtts ) {
        startElement( el.getElementName(),
                      el.getElementAttributes() + moreAtts );
    }

    /**
     * Convenience method to end an element described by
     * an ElementDeclaration.
     *
     * @param  el  element
     */
    protected void endElement( ElementDeclaration el ) {
        endElement( el.getElementName() );
    }

    /**
     * Takes XML text on input (simple HTML-like constructions are understood)
     * and outputs line-formatted text suitable for writing as content of
     * a CDATA declared element.
     *
     * @param   xmlText  input, containing XML markup
     * @return  output, maybe containing XML entity references, but no tags
     */
    private String xmlToCdata( String xmlText ) throws SAXException {
        return formatText( formatter_.formatXML( xmlText, 0 ) );
    }

    /**
     * Returns a unique key relating to a given task and one of its parameters.
     * The result is not primarily designed to be human-readable.
     *
     * @param   task   task
     * @param   param   parameter - if null, the returned reference describes
     *          the task itself
     */
    private static String getParamRef( CeaTask task, CeaParameter param ) {
        return param == null
             ? "TASK-" + task.getName()
             : task.getName() + "_" + param.getName();
    }

    /**
     * Returns an array of the tasks which should form part of the CEA
     * STILTS interface.  The list of tasks and their parameters etc
     * may (should) be doctored for use which may be different from their
     * use on the command line as appropriate.
     *
     * @return   task array
     */
    static CeaTask[] createTaskList() throws LoadException {

        /* Get a map containing each of the known STILTS tasks keyed by
         * name. */
        ObjectFactory<Task> taskFactory = Stilts.getTaskFactory();
        String[] taskNames = taskFactory.getNickNames();
        Map<String,CeaTask> appMap = new HashMap<String,CeaTask>();
        for ( int i = 0; i < taskNames.length; i++ ) {
            String name = taskNames[ i ];
            Task task = taskFactory.createObject( name );

            /* MapperTasks with VariableTablesInput won't work because
             * they construct their argument lists on the fly from other
             * variables. */
            if ( ! ( task instanceof MapperTask &&
                     ((MapperTask) task).getTablesInput()
                                        instanceof VariableTablesInput ) ) {
                appMap.put( name, new CeaTask( task, name ) );
            }
        }

        /* Remove some tasks unsuitable for the CEA installation. */
        String[] removals = new String[] {
            "funcs",
            "sqlclient",
            "sqlupdate",
            "sqlskymatch",
        };
        for ( int i = 0; i < removals.length; i++ ) {
            String rname = removals[ i ];
            if ( appMap.containsKey( rname ) ) {
                appMap.remove( rname );
            }
            else {
                throw new RuntimeException( "Configuration error: no such task "
                                          + rname );
            }
        }

        /* Doctor some of the specific tasks as required; small changes
         * to parameters etc are required for sensible use in a CEA
         * environment. */
        CeaTask tpipe = appMap.get( "tpipe" );
        tpipe.removeParameter( "istream" );

        CeaTask votcopy = appMap.get( "votcopy" );
        votcopy.getParameter( "in" ).setRef( true );
        votcopy.getParameter( "out" ).setOutput( true );

        CeaTask votlint = appMap.get( "votlint" );
        votlint.getParameter( "out" ).setOutput( true );

        /* Prepare and return an array of CeaTask objects sorted by name. */
        String[] appNames = new ArrayList<String>( appMap.keySet() )
                           .toArray( new String[ 0 ] );
        Arrays.sort( appNames );
        CeaTask[] tasks = new CeaTask[ appNames.length ];
        for ( int i = 0; i < appNames.length; i++ ) {
            tasks[ i ] = appMap.get( appNames[ i ] );
        }
        return tasks;
    }

    /**
     * Definition of one of the flag-like parameters which precedes the
     * task name on the stilts command line.
     */
    private abstract class FlagDef {

        /**
         * Returns parameter name, unadorned by "-" signs etc.
         *
         * @return  name
         */
        public abstract String getName();

        /**
         * Outputs a <code>CmdLineParameterDefn</code> element for this flag.
         */
        public abstract void writeParameter();
    }

    /**
     * FlagDef implementation for standard output/error redirects.
     */
    private class RedirectFlagDef extends FlagDef {
        private final String name_;
        private final String description_;
        private final String defaultValue_;

        /**
         * Constructor.
         *
         * @param  name  parameter name, unadorned by "-" signs etc
         * @param  description  parameter UI description
         * @param  defaultValue  parameter default value, if any
         */
        RedirectFlagDef( String name, String description,
                         String defaultValue ) {
            name_ = name;
            description_ = description;
            defaultValue_ = defaultValue;
        }

        public String getName() {
            return name_;
        }

        public void writeParameter() {
            ElementDeclaration paramDecl = config_.getParameterElement();
            StringBuffer attBuf = new StringBuffer()
                .append( formatAttribute( "name", name_ ) )
                .append( formatAttribute( "type", "text" ) );
            if ( paramDecl.hasAttribute( "fileRef" ) ) {
                attBuf.append( formatAttribute( "fileRef", "true" ) );
            }
            if ( paramDecl.hasAttribute( "switchType" ) ) {
                attBuf.append( formatAttribute( "switchType", "normal" ) );
            }
            startElement( paramDecl, attBuf.toString() );
            addElement( "UI_Name", "", name_ );
            addElement( "UI_Description", "", description_ );
            if ( defaultValue_ != null ) {
                addElement( "DefaultValue", "", defaultValue_ );
            }
            endElement( paramDecl );
        }
    }

    /**
     * Does the work for the {@link #main} method but returns a status
     * rather than ever calling <code>System.exit</code>.
     *
     * @param  args  command-line argument list
     */
    private static int runMain( String[] args )
             throws LoadException, SAXException {

        /* Prepare usage message. */
        String cmdname = CeaWriter.class.getName().replaceFirst( "^.*\\.", "" );
        String commonUsage = " [-task <task>] [-redirects]";
        String usage = new StringBuffer()
            .append( "\n   Usage:" )
            .append( "\n      " )
            .append( cmdname )
            .append( " -help" )
            .append( "\n      " )
            .append( cmdname )
            .append( " -impl" )
            .append( ImplementationCeaWriter.getUsage() )
            .append( commonUsage )
            .append( "\n      " )
            .append( cmdname )
            .append( " -service" )
            .append( ServiceCeaWriter.getUsage() )
            .append( commonUsage )
            .append( "\n" )
            .toString();

        /* Prepare command line string for logging etc. */
        StringBuffer cmdBuf = new StringBuffer( CeaWriter.class.getName() );
        for ( int i = 0; i < args.length; i++ ) {
            cmdBuf.append( ' ' ).append( args[ i ] );
        }
        String cmdline = cmdBuf.toString();

        /* Process command line arguments. */
        List<String> argList = new ArrayList<String>( Arrays.asList( args ) );
        CeaTask task1 = null;
        PrintStream out = System.out;
        Boolean isImpl = null;
        boolean redirects = false;
        for ( Iterator<String> it = argList.iterator(); it.hasNext(); ) {
            String arg = it.next();
            if ( "-h".equals( arg ) || "-help".equals( arg ) ) {
                System.out.println( usage );
                return 0;
            }
            else if ( arg.startsWith( "-impl" ) ) {
                it.remove();
                if ( isImpl != null ) {
                    System.err.println( usage );
                    return 1;
                }
                isImpl = Boolean.TRUE;
            }
            else if ( arg.startsWith( "-serv" ) ) {
                it.remove();
                if ( isImpl != null ) {
                    System.err.println( usage );
                    return 1;
                }
                isImpl = Boolean.FALSE;
            }
            else if ( arg.equals( "-task" ) && it.hasNext() ) {
                it.remove();
                String taskName = it.next();
                it.remove();
                if ( task1 != null ) {
                    System.err.println( usage );
                    return 1;
                }
                task1 = new CeaTask( Stilts.getTaskFactory()
                                          .createObject( taskName ),
                                     taskName );
            }
            else if ( arg.equals( "-redirects" ) ) {
                it.remove();
                redirects = true;
            }
        }
        if ( isImpl == null ) {
            System.err.println( usage );
            return 1;
        }
        final CeaTask[] tasks;
        final CeaMetadata meta;
        if ( task1 == null ) {
            tasks = createTaskList();
            meta = CeaMetadata.createStiltsMetadata( tasks );
        }
        else {
            tasks = new CeaTask[] { task1 };
            meta = CeaMetadata.createTaskMetadata( task1 );
        }
        CeaWriter writer = isImpl.booleanValue()
            ? (CeaWriter) new ImplementationCeaWriter( out, tasks, meta,
                                                       redirects, cmdline )
            : (CeaWriter) new ServiceCeaWriter( out, tasks, meta,
                                                redirects, cmdline );

        /* Perform implementation-specific command-line arg configuration. */
        int configStat = writer.configure( argList.toArray( new String[ 0 ] ) );
        if ( configStat != 0 ) {
            System.err.println( usage );
            return configStat;
        }

        /* Do the work. */
        writer.writeDocument();
        return 0;
    }

    /**
     * Main method.  Run with <code>-help</code> for a usage message.
     *
     * @param  args  argument list
     */
    public static void main( String[] args )
            throws LoadException, SAXException {
        int status = runMain( args );
        if ( status != 0 ) {
            System.exit( 1 );
        }
    }
}
