/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.meteoinfo.console.jython;

import org.meteoinfo.console.NameCompletion;
import org.python.core.*;
import org.python.util.PythonInterpreter;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author Yaqiang Wang
 */
public class JIntrospect implements NameCompletion {

    private final Pattern IMPORT_PACKAGE = Pattern.compile("import\\s+(.+)\\.");
    private final Pattern FROM_PACKAGE_IMPORT = Pattern.compile("from\\s+(\\w+(?:\\.\\w+)*)\\.?(?:\\s*import\\s*)?");
    private final PythonInterpreter interp;

    /**
     * Constructor
     *
     * @param interp
     */
    public JIntrospect(PythonInterpreter interp) {
        this.interp = interp;
    }

    /**
     * Complete package name
     *
     * @param target Target
     * @return Package names
     */
    public List<String> completePackageName(String target) {
        String[] targetComponents = target.split("\\.");
        String base = targetComponents[0];
        PySystemState state = interp.getSystemState();
        PyObject importer = state.getBuiltins().__getitem__(Py.newString("__import__"));
        PyObject module = importer.__call__(Py.newString(base));
        if (targetComponents.length > 1) {
            for (int i = 1; i < targetComponents.length; i++) {
                try {
                    module = module.__getattr__(targetComponents[i]);
                } catch (Exception e) {
                    return new ArrayList<>();
                }
            }
        }
        PyList plist = (PyList) module.__dir__();
        if (plist.contains("__all__")) {
            plist = (PyList) module.__getattr__("__all__");
        }
        List<String> list = new ArrayList<>();
        String name;
        for (int i = 0; i < plist.__len__(); i++) {
            name = plist.get(i).toString();
            if (!name.startsWith("__")) {
                list.add(name);
            }
        }
        //list.add("*");

        return list;
    }

    /**
     * Get package name
     *
     * @param command The command
     * @return Package name
     */
    public String getPackageName(String command) {
        Matcher match = IMPORT_PACKAGE.matcher(command);
        if (match.find()) {
            return match.group(1);
        } else {
            match = FROM_PACKAGE_IMPORT.matcher(command);
            if (match.find()) {
                return match.group(1);
            } else {
                return null;
            }
        }
    }
    
    /**
     * Get auto complete list
     * @param command Command string
     * @return Complete list string
     * @throws IOException 
     */
    public List<String> getAutoCompleteList(String command) throws IOException{
        return this.getAutoCompleteList(command, true, true, true);
    }

    /**
     * Get auto complete list
     *
     * @param command The command
     * @param includeMagic
     * @param includeSingle
     * @param includeDouble
     * @return Auto complete list
     * @throws IOException
     */
    public List<String> getAutoCompleteList(String command, boolean includeMagic,
            boolean includeSingle, boolean includeDouble) throws IOException {
        // Temp KLUDGE here rather than in console.py
        //command += ".";
        if (command.startsWith("import ") || command.startsWith("from ")) {
            String target = getPackageName(command);
            if (target == null) {
                return null;
            }
            return completePackageName(target);
        }
        
        String root = this.getRoot(command, ".");
        if (root.isEmpty())
            return null;
        
        try {
            PyObject object = this.interp.eval(root);
            PyList plist = (PyList) object.__dir__();
            if (plist.contains("__all__")) {
                PyList nPlist = (PyList) object.__getattr__("__all__");
                for (int i = 0; i < nPlist.__len__(); i++) {
                    if (! plist.__contains__(nPlist.__getitem__(i))) {
                        plist.add(nPlist.__getitem__(i));
                    }
                }
            }
            List<String> list = new ArrayList<>();
            String name;
            for (int i = 0; i < plist.__len__(); i++) {
                name = plist.get(i).toString();
                if (!name.contains("__")) {
                    list.add(name);
                }
            }
            return list;
        } catch (Exception e){
            return null;
        }
    }
    
    private boolean ispython(Object object){
        if (object instanceof Class)
            return false;
        else if (object instanceof Object)
            return false;
        else if (object instanceof PyReflectedFunction)
            return false;
        else
            return true;
    }

    @Override
    public String[] completeName(String paramString) {
        List<String> names = null;
        try {
            names = this.getAutoCompleteList(paramString, true, true, true);
        } catch (IOException ex) {
            Logger.getLogger(JIntrospect.class.getName()).log(Level.SEVERE, null, ex);
        }
        if (names == null) {
            return null;
        }
        Collections.sort(names);
        return names.toArray(new String[names.size()]);
    }
    
    /**
     * Return the rightmost root portion of an arbitrary Python command.
     * @param command The command
     * @param terminator The terminator - '(' or '.'
     * @return Rightmost root portion
     * @throws IOException
     */
    public String getRoot(String command, String terminator) throws IOException{
        String[] temp = command.split("\n");
        command = temp[temp.length - 1];
        if (command.startsWith("... ")){
            command = command.substring(4);
        }
        command = command.trim();
        if (terminator.equals("(")){
            if (command.endsWith("("))
                command = command.substring(0, command.length() - 1);
        } else {
            command = rtrimTerminus(command, terminator);
            if (terminator.equals(".")) {
                if (command.contains("(")) {
                    command = command.substring(command.lastIndexOf("(") + 1);
                }
            }
        }
        PyList tokens = this.getTokens(command);
        if (tokens == null || tokens.isEmpty())
            return "";
        
        PyTuple token = (PyTuple)tokens.get(tokens.size() - 1);
        if ((int)token.get(0) == 0)
            tokens.remove(tokens.size() - 1);
        
        if (tokens.isEmpty())
            return "";
        
        token = (PyTuple)tokens.get(tokens.size() - 1);
        if (terminator.equals(".") && (!token.get(1).toString().equals(".") ||
                (int)token.get(0) != 51)){
            return "";
        } else {
            if (command.endsWith(terminator)){
                int size = terminator.length();
                command = command.substring(0, command.length() - size);
            }
        }
        
        command = command.trim();
        tokens = this.getTokens(command);
        tokens.reverse();
        
        String line = "";
        Integer start = null;
        String prefix = "";
        String laststring = ".";
        List<String> emptyTypes = new ArrayList<>();
        emptyTypes.add("[]");
        emptyTypes.add("()");
        emptyTypes.add("{}");
        for (int i = 0; i < tokens.size(); i++){
            token = (PyTuple)tokens.get(i);
            int tokentype = (int)token.get(0);
            String tokenstring = token.get(1).toString();
            line = token.get(4).toString();
            if (tokentype == 0)
                continue;
            if (tokentype == 1 || tokentype == 2 || tokentype == 3){
                    if (!laststring.equals(".")){
                        int idx = (int)((PyTuple)token.get(3)).get(1);
                        if (!prefix.isEmpty() && line.substring(idx, idx + 1).equals(" "))
                            prefix = "";
                        break;
                    }
            }
            if (tokentype == 1 || tokentype == 2 || tokentype == 3 || (tokentype == 51 &&
                    tokenstring.equals("."))){
                if (!prefix.isEmpty()){
                    prefix = "";
                    break;
                } else {
                    start = (int)((PyTuple)token.get(2)).get(1);
                }
            } else if (tokenstring.length() == 1 && "[({])}".contains(tokenstring)){
                if (emptyTypes.contains(prefix) && "[({".contains(tokenstring)){
                    break;
                } else {
                    prefix = tokenstring + prefix;
                }
            } else 
                break;
            
            laststring = tokenstring;
        }
        
        if (start == null){
            start = line.length();
        }
        String root = line.substring(start);
        if (emptyTypes.contains(prefix)){
            root = prefix + root;
        }
        
        return root;
    }
    
    /**
     * Return list of token for command.
     * @param command The command
     * @return Token list
     * @throws UnsupportedEncodingException
     */
    public PyList getTokens(String command) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        sb.append("import cStringIO");
        sb.append("\n");
        sb.append("import tokenize");
        sb.append("\n");
        sb.append("command = str('");
        sb.append(command);
        sb.append("')");
        sb.append("\n");
        sb.append("f = cStringIO.StringIO(command)");
        sb.append("\n");
        sb.append("tokens = []");
        sb.append("\n");
        sb.append("def eater(*args):");
        sb.append("\n");
        sb.append("    tokens.append(args)");
        sb.append("\n");
        sb.append("tokenize.tokenize_loop(f.readline, eater)");
        sb.append("\n");
        String code = sb.toString();
        String encoding = "utf-8";
        PythonInterpreter pi = new PythonInterpreter();
        try {
            pi.execfile(new ByteArrayInputStream(code.getBytes(encoding)));
            PyList tokens = (PyList)pi.get("tokens");            
            return tokens;
        } catch (Exception e){
            return null;
        }
    }
    
    /**
     * Return command minus anything that follows the final terminator.
     * @param command The command
     * @param terminator The terminator - '(' or '.'
     * @return Result string
     */
    public String rtrimTerminus(String command, String terminator){
        if ((terminator != null) && (!command.endsWith(terminator))) {
            String[] pieces;
            if (terminator.equals("."))
                pieces = command.split("\\.");
            else
                pieces = command.split(terminator);
            if (pieces.length > 1){
                command = pieces[0];
                for (int i = 1; i < pieces.length - 1; i++){
                    command = command + terminator + pieces[i];
                }
                command = command + terminator;
            }
        }
        return command;
    }
    
    /**
     * For a command, return a tuple of object name, argspec, tip text.
     * @param command Command string
     * @return Tip text
     * @throws IOException
     */
    public String[] getCallTipJava(String command) throws IOException{
        String[] callTip = new String[]{"","",""};
        String root = getRoot(command, "(");
        try {
            PyObject object = this.interp.eval(root);
            if (object instanceof PyFunction){
                PyFunction func = (PyFunction)object;
                String name = func.__doc__.toString();
                callTip[2] = name;
            } else if (object instanceof PyMethod){
                PyMethod method = (PyMethod)object;
                PyFunction func = (PyFunction)method.__func__;
                String name = func.__doc__.toString();
                callTip[2] = name;
            } else {
                callTip[2] = object.toString();
            }
        } catch (Exception e){
            
        }
        
        return callTip;
    }
    
    @Override
    public String[] getTip(String command){
        try {
            return this.getCallTipJava(command);
        } catch (IOException ex) {
            Logger.getLogger(JIntrospect.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }
}
