/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 jena.cmd;

import static java.nio.file.Files.readAllBytes;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList ;
import java.util.Arrays ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Map ;

import org.apache.jena.atlas.logging.Log ;

/**
 * Command line, using the common named/positional arguments paradigm
 * (also called options/arguments).
 */
public class CmdLineArgs extends CommandLineBase {
    public CmdLineArgs(String[] args) {
        super(args) ;
    }

    private boolean processedArgs = false ;
    protected Map<String, ArgDecl> argMap = new HashMap<>() ;   // Map from string name to ArgDecl
    protected Map<String, Arg> args = new HashMap<>() ;         // Name to Arg
    protected List<String> positionals = new ArrayList<>() ;    // Positional arguments as strings.
    
    public void process() throws IllegalArgumentException
    {
        processedArgs = true ;
        apply(new ArgProcessor()) ;
    }
    
    // ---- Setting the ArgDecls

    /** Add an argument to those to be accepted on the command line.
     * @param argName Name
     * @param hasValue True if the command takes a (string) value
     * @return The CommandLine processor object
     */

    public CmdLineArgs add(String argName, boolean hasValue) {
        return add(new ArgDecl(hasValue, argName)) ;
    }

    /** Add an argument to those to be accepted on the command line.
     *  Argument order reflects ArgDecl.
     * @param hasValue True if the command takes a (string) value
     * @param argName Name
     * @return The CommandLine processor object
     */
   
    public CmdLineArgs add(boolean hasValue, String argName) {
        return add(new ArgDecl(hasValue, argName)) ;
    }

    /** Add an argument object
     * @param arg Argument to add
     * @return The CommandLine processor object
     */
   
    public CmdLineArgs add(ArgDecl arg) {
        for (Iterator<String> iter = arg.names(); iter.hasNext();) {
            String name = iter.next() ;
            if ( argMap.containsKey(name) )
                Log.warn(this, "Argument '" + name + "' already added") ;
            argMap.put(name, arg) ;
        }
        return this ;
    }
    
    /**
     * Add a positional parameter
     * @param value
     * @return this object
     */
    public CmdLineArgs addPositional(String value) {
        positionals.add(value) ;
        return this ;
    }
    
    /**
     * Add a named argument which has no value.
     * @param name
     * @return this
     */
    public CmdLineArgs addArg(String name) {
        return addArg(name, null) ;
    }

    /**
     * Add a named argument/value pair
     * @param name
     * @param value
     * @return this object
     */
    public CmdLineArgs addArg(String name, String value) {
        if ( !args.containsKey(name) )
            args.put(name, new Arg(name)) ;
        Arg arg = args.get(name) ;
        return addArgWorker(arg, value) ;
    }

    private CmdLineArgs addArgWorker(Arg arg, String value) {
        ArgDecl argDecl = argMap.get(arg.getName()) ;

        if ( !argDecl.takesValue() && value != null )
            throw new IllegalArgumentException("No value for argument: " + arg.getName()) ;

        if ( argDecl.takesValue() ) {
            if ( value == null )
                throw new IllegalArgumentException("No value for argument: " + arg.getName()) ;

            arg.setValue(value) ;
            arg.addValue(value) ;
        }

        return this ;
    }

    // ---- Indirection

    static final String DefaultIndirectMarker = "@" ;
    
    public boolean matchesIndirect(String s) { return matchesIndirect(s, DefaultIndirectMarker) ; }
    public boolean matchesIndirect(String s, String marker) { return s.startsWith(marker) ; }
    
    public String indirect(String s) { return indirect(s, DefaultIndirectMarker) ; }
    
    public String indirect(String s, String marker) {
        if ( !matchesIndirect(s, marker) )
            return s ;
        s = s.substring(marker.length()) ;
        try {
			return new String(readAllBytes(Paths.get(s)));
		} catch (IOException e) {
			throw new CmdException("Could not read from: " + s, e);
		}
    }

    // ---- Argument access
    
    /** Test whether an argument was seen. */

    public boolean contains(ArgDecl argDecl)    { return getArg(argDecl) != null ; }
    
    /** Test whether an argument was seen. */

    public boolean contains(String s)           { return getArg(s) != null ; }
    
    /** Test whether an argument was seen more than once */ 
    public boolean containsMultiple(String s)   { return getValues(s).size() > 1 ; }
    
    /** Test whether an argument was seen more than once */ 
    public boolean containsMultiple(ArgDecl argDecl) { return getValues(argDecl).size() > 1 ; }
    
    public boolean hasArgs() { return args.size() > 0 ; }
    
    /** Test whether the command line had a particular argument
     * 
     * @param argName
     * @return this object
     */
    public boolean hasArg(String argName) { return getArg(argName) != null ; }

    /** Test whether the command line had a particular argument
     * 
     * @param argDecl
     * @return true or false
     */
    
    public boolean hasArg(ArgDecl argDecl) { return getArg(argDecl) != null ; }

    
    /** Get the argument associated with the argument declaration.
     *  Actually returns the LAST one seen
     *  @param argDecl Argument declaration to find
     *  @return Last argument that matched.
     */
    
    public Arg getArg(ArgDecl argDecl) {
        Arg arg = null ;
        for ( Arg a : args.values() )
        {
            if ( argDecl.matches( a ) )
            {
                arg = a;
            }
        }
        return arg ;
    }
    
    /** Get the argument associated with the argument name.
     *  Actually returns the LAST one seen
     *  @param argName Argument name
     *  @return Last argument that matched.
     */
    public Arg getArg(String argName) {
        argName = ArgDecl.canonicalForm(argName) ;
        return args.get(argName) ;
    }
    
    /**
     * Returns the value (a string) for an argument with a value - 
     * returns null for no argument and no value.  
     * @param argDecl
     * @return String
     */
    public String getValue(ArgDecl argDecl) {
        Arg arg = getArg(argDecl) ;
        if ( arg == null )
            return null ;
        if ( arg.hasValue() )
            return arg.getValue() ;
        return null ;
    }

    /**
     * Returns the value (a string) for an argument with a value - 
     * returns null for no argument and no value.  
     * @param argName
     * @return String
     */
    public String getValue(String argName) {
        Arg arg = getArg(argName) ;
        if ( arg == null )
            return null ;
        return arg.getValue() ;
    }
    
    /** Is the value something that looks like "true" or "yes"? */
    public boolean hasValueOfTrue(ArgDecl argDecl) {
        String x = getValue(argDecl) ;
        if ( x == null )
            return false ;
        if ( x.equalsIgnoreCase("true") || x.equalsIgnoreCase("t") ||
             x.equalsIgnoreCase("yes")  || x.equalsIgnoreCase("y") )
            return true ;
        return false ;
    }

    /** Is the value something that looks like "false" or "no"? */
    public boolean hasValueOfFalse(ArgDecl argDecl) {
        String x = getValue(argDecl) ;
        if ( x == null )
            return false ;
        if ( x.equalsIgnoreCase("false") || x.equalsIgnoreCase("f") ||
             x.equalsIgnoreCase("no") || x.equalsIgnoreCase("n") )
            return true ;
        return false ;
    }
    
    /**
     * Returns all the values (0 or more strings) for an argument. 
     * @param argDecl
     * @return List
     */
    public List<String> getValues(ArgDecl argDecl) {
        Arg arg = getArg(argDecl) ;
        if ( arg == null )
            return new ArrayList<>() ;
        return arg.getValues() ;
    }

    /**
     * Returns all the values (0 or more strings) for an argument. 
     * @param argName
     * @return List
     */
    public List<String> getValues(String argName) {
        Arg arg = getArg(argName) ;
        if ( arg == null )
            return new ArrayList<>() ;
        return arg.getValues() ;
    }
    
   // ---- Positional 
    /** Get the i'th positional argument (indexed from 0)*/
    public String getPositionalArg(int i) {
        return positionals.get(i) ;
    }

    /** Return the number of positional arguments */  
    public int getNumPositional() {
        return positionals.size() ;
    }

    public boolean hasPositional() {
        return positionals.size() > 0 ;
    }

    public List<String> getPositional() {
        return positionals ;
    }

    /** Return the positional arguments or "-" to indicate stdin */
    public List<String> getPositionalOrStdin() {
        if ( !positionals.isEmpty() )
            return Collections.unmodifiableList(positionals) ;
        List<String> x = Arrays.asList("-") ;
        return Collections.unmodifiableList(x) ;
    }
    
    // ----
    
    /**
     * Handle an unrecognised argument; default is to throw an exception
     * @param argStr The string image of the unrecognised argument
     */
    protected void handleUnrecognizedArg( String argStr ) {
        throw new CmdException("Unknown argument: "+argStr) ;
    }
    
    @Override
    public String toString() {
        if ( !processedArgs )
            return super.toString() ;
        String str = "" ;
        String sep = "" ;
        for ( String k : args.keySet() )
        {
            Arg a = args.get( k );
            str = str + sep + a;
            sep = " ";
        }
        sep = " -- " ;
        for ( String v : positionals )
        {
            str = str + sep + v;
            sep = " ";
        }
        return str ;
    }

    // ---- Process arguments after low level parsing and after ArgDecls added.
    class ArgProcessor implements ArgProc {
        boolean nextArgProcessed      = false ;
        boolean positionalArgsStarted = false ;

        @Override
        public void startArgs() {
            nextArgProcessed = false ;
            positionalArgsStarted = false ;
        }

        @Override
        public void finishArgs() {}

        @Override
        public void arg(String argStr, int i) {
            if ( nextArgProcessed ) {
                nextArgProcessed = false ;
                return ;
            }

            if ( positionalArgsStarted ) {
                addPositional(argStr) ;
                return ;
            }

            if ( argStr.equals("-") || argStr.equals("--") ) {
                positionalArgsStarted = true ;
                return ;
            }

            if ( !argStr.startsWith("-") ) {
                // End of flags, start of positional arguments
                positionalArgsStarted = true ;
                addPositional(argStr) ;
                return ;
            }

            argStr = ArgDecl.canonicalForm(argStr) ;
            if ( !argMap.containsKey(argStr) ) {
                handleUnrecognizedArg(argStr) ;
                return ;
            }

            // Recognized flag
            ArgDecl argDecl = argMap.get(argStr) ;

            if ( argDecl.takesValue() ) {
                String val = getArg(i + 1) ;
                // Use first name as the canonical one.
                String x = argDecl.getKeyName() ;
                addArg(x, val) ;
                nextArgProcessed = true ;
            } else
                addArg(argStr) ;
        }
    }
}
