/*
 * Copyright (C) 2008 Helge Hess
 *
 * This file is part of JOPE.
 *
 * JOPE is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2, or (at your option) any later version.
 *
 * JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JOPE; see the file COPYING. If not, write to the Free Software
 * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package org.opengroupware.jope.jsapp;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.Wrapper;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;

/**
 * JSKeyValueCodingScope
 * <p>
 * This is a combination of a *sealed* JavaScript scope plus a cache of KVC
 * wrappers.
 */
public abstract class JSKeyValueCodingScope extends NSObject {

  public final ScriptableObject scope;
  
  public JSKeyValueCodingScope(ScriptableObject _scope) {
    this.scope = _scope;
  }
  
  /* factory */
  
  public static JSKeyValueCodingScope wrap(ScriptableObject _scope) {
    if (_scope == null)
      return null;
    
    Map<String, Object> kvc = extractKeyValueCodingMap(_scope);
    
    if (kvc == null)
      return new JSKeyValueCodingEmptyScope(_scope);
    
    // TBD: a special imp for small sets to avoid a hashmap
    
    return new JSKeyValueCodingMapScope(_scope, kvc); 
  }

  
  /* KVC */
  
  public abstract Object valueForKeyInScope(String _key, Scriptable _thisScope);
  public abstract Set<String> keySet();
  
  protected static Map<String, Object> extractKeyValueCodingMap
    (ScriptableObject _scope)
  {
    if (_scope == null)
      return null;
    
    Map<String, Object> keyToObject = new HashMap<String, Object>();

    // TBD: this excludes slots which are not enumerated, which should be OK
    for (Object slotId: _scope.getIds()) {
      if (!(slotId instanceof String))
        continue;
      
      String slotIdS = (String)slotId;
      
      /* Using the scope as the start is not necessarily the best idea,
       * but should be OK here.
       */
      Object v = _scope.get(slotIdS, _scope);
      if (v == null || v == Scriptable.NOT_FOUND)
        continue; /* does not contain the slot, which is OK */
      
      
      /* OK, got a slot value, now it gets interesting. If its a function,
       * we store it. It must be evaluated each time.
       * If its *not*, we already unwrap the value and cache it!
       */
      if (v instanceof Callable) {
        // TBD: do we need to protect 'set' accessors? Currently we could call
        //      valueForKey("setAddress").
        // TBD: should we support getX style GET accessors? Or maybe use only
        //      getX style ones (would allow us to have an ivar slot with the
        //      same name). => no we need no-get ones for actions (doIt())
        /* Note: we cannot bind the function yet, because the binder is per
         *       component. (hm, maybe we need per-object caches?)
         */
        //System.err.println("MAP " + slotIdS + " to " + v);
        keyToObject.put(slotIdS, v);
      }
      else {
        /* its a regular variable slot */
        
        if (v instanceof Wrapper)
          v = ((Wrapper)v).unwrap();
        else if (v instanceof Undefined)
          continue; /* we do not expose JavaScript Undefined values */
        else {
          /* Note: this seems to convert Undefined to a String */
          v = Context.jsToJava(v, Object.class);
        }
        
        /* Note: we do not distinguish between missing and null */
        keyToObject.put(slotIdS, v);
        //System.err.println("  key[" + slotIdS + "]GOT VALUE: " + v);
      }
    }
    
    return (keyToObject.size() == 0) ? null : keyToObject;
  }

  /* description */

  @Override
  public void appendAttributesToDescription(final StringBuilder _d) {
    super.appendAttributesToDescription(_d);

    Set<String> keys = this.keySet();
    if (keys != null && keys.size() > 0) {
      _d.append(" keys=");
      _d.append(UString.componentsJoinedByString(keys,","));
    }
    else
      _d.append(" no-keys");
    
    if (this.scope != null) {
      _d.append(" scope[");
      _d.append(this.scope.getClass().getSimpleName());
      _d.append(']');
    }
    else
      _d.append(" no-scope");
  }
  
  
  /* class cluster */
  
  private static class JSKeyValueCodingMapScope extends JSKeyValueCodingScope {
    
    final protected Map<String, Object> keyToObject;
    
    public JSKeyValueCodingMapScope
      (ScriptableObject _scope, Map<String, Object> _kvc)
    {
      super(_scope);
      this.keyToObject = _kvc;
    }

    @Override
    public Set<String> keySet() {
      return this.keyToObject.keySet();
    }
    
    @Override
    public Object valueForKeyInScope(String _key, Scriptable _thisScope) {
      if (this.keyToObject == null || _key == null)
        return null;
      
      /* Using the scope as the start is not necessarily the best idea,
       * but should be OK here.
       */
      Object v = this.keyToObject.get(_key);
      if (v == null)
        return null;
      
      /* check whether the value is a getter */

      if (v instanceof Callable) {
        // TBD: do we need to bind the function? Doesn't look like!
        // TBD: we could pass in various args
        
        //System.err.println("CALL " + _key + " IN scope: " + _thisScope + 
        //    " AS: " + v);

        /* bindings is not necessary because JSComp.valueForKey() already
         * calls the function with a proper scope/this (_thisScope)
         */
        //if (!(v instanceof JSBoundFunction) && (v instanceof Function)) {
        //  v = new JSBoundFunction(_thisScope, (Function)v);
        //}
        
        v = ((Callable)v).call(Context.getCurrentContext(),
            _thisScope /* scope (where to lookup variables) */,
            _thisScope /* this  */,
            JSUtil.emptyArgs);
      }


      if (v instanceof Wrapper)
        v = ((Wrapper)v).unwrap();
      else if (v instanceof Undefined)
        v = null; /* we do not expose JavaScript Undefined values */
      else {
        /* Note: this seems to convert Undefined to a String */
        v = Context.jsToJava(v, Object.class);
      }
      //System.err.println("  GOT VALUE: " + v);

      return v;
    }
  }
  
  
  /* empty scope (no KVC keys) */
  
  private static class JSKeyValueCodingEmptyScope extends JSKeyValueCodingScope {

    public JSKeyValueCodingEmptyScope(ScriptableObject _scope) {
      super(_scope);
    }
    
    @Override
    public Set<String> keySet() {
      return null;
    }
    
    @Override
    public Object valueForKeyInScope(String _key, Scriptable _thisScope) {
      return null; // contains no KVC keys
    }
  }
}
