/*
 * 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 org.netbeans.modules.javascript2.knockout.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.types.api.DeclarationScope;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.Occurrence;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.javascript2.model.spi.FunctionArgument;
import org.netbeans.modules.javascript2.model.spi.FunctionInterceptor;
import org.netbeans.modules.javascript2.model.spi.ModelElementFactory;
import org.netbeans.modules.parsing.api.Snapshot;

/**
 *
 * @author Petr Hejl
 */
@FunctionInterceptor.Registration(priority = 200)
public class KnockoutExportInterceptor implements FunctionInterceptor {

    private static final Logger LOGGER = Logger.getLogger(KnockoutExportInterceptor.class.getName());

    private static final String GLOBAL_KO_OBJECT = "ko"; // NOI18N

    private static final Pattern NAME_PATTERN = Pattern.compile("ko\\.(exportSymbol|exportProperty)"); // NOI18N

    @Override
    public Pattern getNamePattern() {
        return NAME_PATTERN;
    }

    @Override
    public Collection<TypeUsage> intercept(Snapshot snapshot, String functionName, JsObject globalObject, DeclarationScope scope,
            ModelElementFactory factory, Collection<FunctionArgument> args) {

        if (args.size() == 3) {
            Iterator<FunctionArgument> iterator = args.iterator();
            FunctionArgument objectArgument = iterator.next();
            FunctionArgument nameArgument = iterator.next();
            FunctionArgument valueArgument = iterator.next();

            int offset = nameArgument.getOffset();

            JsObject object = null;
            if (objectArgument.getKind() == FunctionArgument.Kind.REFERENCE) {
                List<String> identifiers = (List<String>) objectArgument.getValue();
                JsObject ref = getReference(scope, identifiers, false);
                if (ref != null) {
                    JsObject found = findJsObjectByAssignment(globalObject, ref, offset);
                    if (found != null) {
                        ref = found;
                    }
                }
                object = ref;
            }
            JsObject value = null;
            if (valueArgument.getKind() == FunctionArgument.Kind.REFERENCE) {
                List<String> identifiers = (List<String>) valueArgument.getValue();
                JsObject ref = getReference(scope, identifiers, true);
                if (ref != null) {
                    JsObject found = findJsObjectByAssignment(globalObject, ref, offset);
                    if (found != null) {
                        ref = found;
                    }
                }
                value = ref;
            }
            String name = (String) nameArgument.getValue();
            OffsetRange offsetRange = new OffsetRange(offset, offset + name.length());
            if (object != null && value != null) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.log(Level.FINE, "Exporting property {0} to {1} as {2}",
                            new Object[] {name, object.getFullyQualifiedName(), value.getFullyQualifiedName()});
                }
                EnumSet modifiers = EnumSet.copyOf(value.getModifiers());
                modifiers.remove(Modifier.STATIC);
                object.addProperty(name,
                        factory.newReference(object, name, offsetRange, value, true, modifiers));
            }
        } else if (args.size() == 2) {
            JsObject ko = globalObject.getProperty(GLOBAL_KO_OBJECT); // NOI18N
            if (ko == null) {
                ko = factory.newObject(globalObject, GLOBAL_KO_OBJECT, OffsetRange.NONE, true);
                globalObject.addProperty(GLOBAL_KO_OBJECT, ko);
            }

            Iterator<FunctionArgument> iterator = args.iterator();
            FunctionArgument nameArgument = iterator.next();
            FunctionArgument valueArgument = iterator.next();

            int offset = nameArgument.getOffset();
            if (nameArgument.getKind() == FunctionArgument.Kind.STRING) { // NOI18N
                JsObject parent = ko;

                String[] names = ((String) nameArgument.getValue()).split("\\."); // NOI18N
                for (int i = 0; i < names.length - 1; i++) {
                    String name = names[i];
                    if (i == 0 && GLOBAL_KO_OBJECT.equals(name)) {
                        continue;
                    }
                    JsObject child = parent.getProperty(name);
                    OffsetRange offsetRange = new OffsetRange(offset, offset + name.length());
                    if (child == null) {
                        child = factory.newObject(parent, name, offsetRange, true);
                        parent.addProperty(name, child);
                    } else if (!child.isDeclared()) {
                        JsObject newJsObject = factory.newObject(parent, name, offsetRange, true);
                        parent.addProperty(name, newJsObject);
                        for (Occurrence occurrence : child.getOccurrences()) {
                            newJsObject.addOccurrence(occurrence.getOffsetRange());
                        }
                        newJsObject.addOccurrence(child.getDeclarationName().getOffsetRange());
                        child = newJsObject;
                    }
                    parent = child;
                }

                String name = names[names.length - 1];
                if (valueArgument.getKind() == FunctionArgument.Kind.REFERENCE) {
                    List<String> identifiers = (List<String>) valueArgument.getValue();
                    JsObject value = getReference(scope, identifiers, false);
                    if (value != null) {
                        JsObject found = findJsObjectByAssignment(globalObject, value, offset);
                        if (found != null && found.isDeclared()) {
                            value = found;
                        } else {
                            int levelUp = identifiers.size() - 1;
                            JsObject foundParent = null;
                            JsObject valueParent = value.getParent();
                            while (valueParent != null) {
                                foundParent = findJsObjectByAssignment(globalObject, valueParent, offset);
                                if (foundParent != null) {
                                    break;
                                }
                                levelUp--;
                                valueParent = valueParent.getParent();
                            }
                            if (foundParent != null) {
                                boolean skip = true;
                                for (int i = levelUp; i < identifiers.size(); i++) {
                                    JsObject property = foundParent.getProperty(identifiers.get(i));
                                    if (property == null) {
                                        skip = false;
                                        break;
                                    }
                                    foundParent = property;
                                }
                                if (skip) {
                                    return Collections.emptyList();
                                }
                            }
                        }

                        OffsetRange offsetRange = new OffsetRange(offset, offset + name.length());
                        JsObject property = parent.getProperty(name);
                        if (property != null) {
                            // XXX is looks like value is artificial
                            if ((property instanceof JsFunction) && !(value instanceof JsFunction)) {
                                value = property;
                            } else {
                                Map<String, JsObject> current = new HashMap<String, JsObject>(property.getProperties());
                                current.keySet().removeAll(value.getProperties().keySet());
                                for (Map.Entry<String, JsObject> entry : current.entrySet()) {
                                    // XXX reference ?
                                    value.addProperty(entry.getKey(), entry.getValue());
                                }
                            }
                        }

                        parent.addProperty(name, factory.newReference(
                                parent, name, offsetRange, value, true, null));
                    }
                }

            }
        }
        return Collections.emptyList();
    }

    private static JsObject getReference(DeclarationScope scope,
            List<String> identifier, boolean searchPrototype) {

        if ("this".equals(identifier.get(0))) { // NOI18N
            // XXX this is not exactly right as it is evaluated at runtime
            return (JsObject) scope;
        }
        DeclarationScope currentScope = scope;
        while (currentScope != null) {
            JsObject ret = getReference((JsObject) currentScope, identifier);
            if (ret != null) {
                return ret;
            }
            currentScope = currentScope.getParentScope();
        }
        if (searchPrototype && identifier.size() > 1) {
            List<String> prototype = new ArrayList<String>(identifier);
            prototype.add(prototype.size() - 1, "prototype"); // NOI18N
            return getReference(scope, prototype, false);
        }
        return null;
    }

    private static JsObject getReference(JsObject object, List<String> identifier) {
        // XXX performance
        if (object == null) {
            return null;
        }
        if (identifier.isEmpty()) {
            return object;
        }
        return getReference(object.getProperty(identifier.get(0)),
                identifier.subList(1, identifier.size()));
    }

    private static JsObject findJsObjectByAssignment(JsObject globalObject,
            JsObject value, int offset) {

        return findJsObjectByAssignment(globalObject, value, offset, true);
    }

    private static JsObject findJsObjectByAssignment(JsObject globalObject, JsObject value,
            int offset, boolean searchPrototype) {

        if (value == null) {
            return null;
        }

        JsObject ret = null;
        Collection<? extends TypeUsage> assigments = value.getAssignmentForOffset(offset);
        if (assigments.size() == 1) {
            ret = findJsObjectByName(globalObject,
                    assigments.iterator().next().getType());
        }
        // XXX multiple assignments

        if (ret == null && searchPrototype) {
            String fqn = value.getFullyQualifiedName();
            int index = fqn.lastIndexOf('.');
            if (index > 0) {
                fqn = fqn.substring(0, index) + ".prototype" + fqn.substring(index);
                JsObject obj = findJsObjectByName(globalObject, fqn);
                if (obj != null) {
                    ret = findJsObjectByAssignment(globalObject, obj, offset, false);
                }
            }
        }
        return ret;
    }

    private static JsObject findJsObjectByName(JsObject global, String fqName) {
        JsObject result = global;
        JsObject property = result;
        for (StringTokenizer stringTokenizer = new StringTokenizer(fqName, "."); stringTokenizer.hasMoreTokens() && result != null;) {
            String token = stringTokenizer.nextToken();
            property = result.getProperty(token);
            if (property == null) {
                result = (result instanceof JsFunction)
                        ? ((JsFunction)result).getParameter(token)
                        : null;
                if (result == null) {
                    break;
                }
            } else {
                result = property;
            }
        }
        return result;
    }
}
