/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Contributor(s):
 *   Norris Boyd
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript;

/**
 * This class implements generator objects. See
 * http://developer.mozilla.org/en/docs/New_in_JavaScript_1.7#Generators
 *
 * @author Norris Boyd
 */
public final class NativeGenerator extends IdScriptableObject {
    private static final long serialVersionUID = 1645892441041347273L;

    private static final Object GENERATOR_TAG = "Generator";

    static NativeGenerator init(ScriptableObject scope, boolean sealed) {
        // Generator
        // Can't use "NativeGenerator().exportAsJSClass" since we don't want
        // to define "Generator" as a constructor in the top-level scope.

        NativeGenerator prototype = new NativeGenerator();
        if (scope != null) {
            prototype.setParentScope(scope);
            prototype.setPrototype(getObjectPrototype(scope));
        }
        prototype.activatePrototypeMap(MAX_PROTOTYPE_ID);
        if (sealed) {
            prototype.sealObject();
        }

        // Need to access Generator prototype when constructing
        // Generator instances, but don't have a generator constructor
        // to use to find the prototype. Use the "associateValue"
        // approach instead.
        if (scope != null) {
            scope.associateValue(GENERATOR_TAG, prototype);
        }

        return prototype;
    }

    /**
     * Only for constructing the prototype object.
     */
    private NativeGenerator() { }

    public NativeGenerator(Scriptable scope, NativeFunction function,
                           Object savedState)
    {
        this.function = function;
        this.savedState = savedState;
        // Set parent and prototype properties. Since we don't have a
        // "Generator" constructor in the top scope, we stash the
        // prototype in the top scope's associated value.
        Scriptable top = ScriptableObject.getTopLevelScope(scope);
        this.setParentScope(top);
        NativeGenerator prototype = (NativeGenerator)
            ScriptableObject.getTopScopeValue(top, GENERATOR_TAG);
        this.setPrototype(prototype);
    }

    public static final int GENERATOR_SEND  = 0,
                            GENERATOR_THROW = 1,
                            GENERATOR_CLOSE = 2;

    @Override
    public String getClassName() {
        return "Generator";
    }

    /**
     * Close the generator if it is still open.
     */
    @Override
    protected void finalize() throws Throwable {
        if (savedState != null) {
            // This is a little tricky since we are most likely running in
            // a different thread. We need to get a Context to run this, and
            // we must call "doTopCall" since this will likely be the outermost
            // JavaScript frame on this thread.
            Context cx = Context.getCurrentContext();
            ContextFactory factory = cx != null ? cx.getFactory()
                                                : ContextFactory.getGlobal();
            factory.call(new CloseGeneratorAction(this));
        }
    }

    private static class CloseGeneratorAction implements ContextAction {
        private NativeGenerator generator;

        CloseGeneratorAction(NativeGenerator generator) {
            this.generator = generator;
        }

        public Object run(Context cx) {
            Scriptable scope = ScriptableObject.getTopLevelScope(generator);
            Callable closeGenerator = new Callable() {
                public Object call(Context cx, Scriptable scope,
                                   Scriptable thisObj, Object[] args) {
                     return ((NativeGenerator)thisObj).resume(cx, scope,
                             GENERATOR_CLOSE, new GeneratorClosedException());
                }
            };
            return ScriptRuntime.doTopCall(closeGenerator, cx, scope,
                                           generator, null);
        }
    }

    @Override
    protected void initPrototypeId(int id) {
        String s;
        int arity;
        switch (id) {
          case Id_close:          arity=1; s="close";          break;
          case Id_next:           arity=1; s="next";           break;
          case Id_send:           arity=0; s="send";           break;
          case Id_throw:          arity=0; s="throw";          break;
          case Id___iterator__:   arity=1; s="__iterator__";   break;
          default: throw new IllegalArgumentException(String.valueOf(id));
        }
        initPrototypeMethod(GENERATOR_TAG, id, s, arity);
    }

    @Override
    public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope,
                             Scriptable thisObj, Object[] args)
    {
        if (!f.hasTag(GENERATOR_TAG)) {
            return super.execIdCall(f, cx, scope, thisObj, args);
        }
        int id = f.methodId();

        if (!(thisObj instanceof NativeGenerator))
            throw incompatibleCallError(f);

        NativeGenerator generator = (NativeGenerator) thisObj;

        switch (id) {

          case Id_close:
            // need to run any pending finally clauses
            return generator.resume(cx, scope, GENERATOR_CLOSE,
                                    new GeneratorClosedException());

          case Id_next:
            // arguments to next() are ignored
            generator.firstTime = false;
            return generator.resume(cx, scope, GENERATOR_SEND,
                                    Undefined.instance);

          case Id_send: {
            Object arg = args.length > 0 ? args[0] : Undefined.instance;
            if (generator.firstTime && !arg.equals(Undefined.instance)) {
                throw ScriptRuntime.typeError0("msg.send.newborn");
            }
            return generator.resume(cx, scope, GENERATOR_SEND, arg);
          }

          case Id_throw:
            return generator.resume(cx, scope, GENERATOR_THROW,
                args.length > 0 ? args[0] : Undefined.instance);

          case Id___iterator__:
            return thisObj;

          default:
            throw new IllegalArgumentException(String.valueOf(id));
        }
    }

    private Object resume(Context cx, Scriptable scope, int operation,
                          Object value)
    {
        if (savedState == null) {
            if (operation == GENERATOR_CLOSE)
                return Undefined.instance;
            Object thrown;
            if (operation == GENERATOR_THROW) {
                thrown = value;
            } else {
                thrown = NativeIterator.getStopIterationObject(scope);
            }
            throw new JavaScriptException(thrown, lineSource, lineNumber);
        }
        try {
            synchronized (this) {
              // generator execution is necessarily single-threaded and
              // non-reentrant.
              // See https://bugzilla.mozilla.org/show_bug.cgi?id=349263
              if (locked)
                  throw ScriptRuntime.typeError0("msg.already.exec.gen");
              locked = true;
            }
            return function.resumeGenerator(cx, scope, operation, savedState,
                                            value);
        } catch (GeneratorClosedException e) {
            // On closing a generator in the compile path, the generator
            // throws a special exception. This ensures execution of all pending
            // finalizers and will not get caught by user code.
            return Undefined.instance;
        } catch (RhinoException e) {
            lineNumber = e.lineNumber();
            lineSource = e.lineSource();
            savedState = null;
            throw e;
        } finally {
            synchronized (this) {
              locked = false;
            }
            if (operation == GENERATOR_CLOSE)
                savedState = null;
        }
    }

// #string_id_map#

    @Override
    protected int findPrototypeId(String s) {
        int id;
// #generated# Last update: 2007-06-14 13:13:03 EDT
        L0: { id = 0; String X = null; int c;
            int s_length = s.length();
            if (s_length==4) {
                c=s.charAt(0);
                if (c=='n') { X="next";id=Id_next; }
                else if (c=='s') { X="send";id=Id_send; }
            }
            else if (s_length==5) {
                c=s.charAt(0);
                if (c=='c') { X="close";id=Id_close; }
                else if (c=='t') { X="throw";id=Id_throw; }
            }
            else if (s_length==12) { X="__iterator__";id=Id___iterator__; }
            if (X!=null && X!=s && !X.equals(s)) id = 0;
            break L0;
        }
// #/generated#
        return id;
    }

    private static final int
        Id_close                 = 1,
        Id_next                  = 2,
        Id_send                  = 3,
        Id_throw                 = 4,
        Id___iterator__          = 5,
        MAX_PROTOTYPE_ID         = 5;

// #/string_id_map#
    private NativeFunction function;
    private Object savedState;
    private String lineSource;
    private int lineNumber;
    private boolean firstTime = true;
    private boolean locked;

    public static class GeneratorClosedException extends RuntimeException {
        private static final long serialVersionUID = 2561315658662379681L;
    }
}
