/*
 * Copyright (C) 2007 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.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ConcurrentHashMap;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.Wrapper;
import org.opengroupware.jope.appserver.core.WOComponent;
import org.opengroupware.jope.appserver.core.WOComponentDefinition;
import org.opengroupware.jope.appserver.core.WOContext;
import org.opengroupware.jope.appserver.core.WOResourceManager;
import org.opengroupware.jope.appserver.publisher.IJoContext;
import org.opengroupware.jope.appserver.templates.WOTemplate;
import org.opengroupware.jope.appserver.templates.WOWrapperTemplateBuilder;
import org.opengroupware.jope.ofs.OFSWOComponent;
import org.opengroupware.jope.ofs.fs.IOFSFileInfo;
import org.opengroupware.jope.ofs.fs.OFSHostFileInfo;

/**
 * JSJoComponent
 * <p>
 * A callable which works on a component reachable via its File wrapper.
 */
public class JSJoComponent extends OFSWOComponent {
  // TBD: maybe this is superflous and can be replaced with JoPageInvocation,
  //      at least it does not contain anything JS
  
  protected WOComponentDefinition cdef;
  protected WOComponent component;
  
  /* accessors */

  public File file() {
    IOFSFileInfo info = this.fileInfo();
    return (info instanceof OFSHostFileInfo)
      ? ((OFSHostFileInfo)info).getFile() : null;
  }
  
  @Override
  public Object postProcessCallResult
    (Object _object, Object _result, IJoContext _ctx)
  {
    /* post process results */
    // Note: this is also done in JSComponent, we do it here for @action.
    
    if (_result instanceof Wrapper)
      _result = ((Wrapper)_result).unwrap();
    else if (_result == Scriptable.NOT_FOUND)
      _result = null;

    if (_result == null || _result instanceof Undefined) {
      /* Rhino apparently returns 'Undefined' if the function had no explicit
       * return. I thought JS would return the last expression?
       */
      _result = (_ctx instanceof WOContext) ? ((WOContext)_ctx).page() : null;
    }
    
    return _result;
  }

  /* being a component definition */

  @Override
  public Class lookupComponentClass(String _name, WOResourceManager _rm) {
    return JSComponent.class;
  }

  @Override
  public URL templateURL(String _name) {
    URL url = fileAsURL(new File(this.file(), "Component.html"));
    if (url == null)
      url = fileAsURL(new File(this.file(), _name + ".html"));
    return url;
  }
  @Override
  public URL wodURL(String _name) {
    URL url = fileAsURL(new File(this.file(), "Component.wod"));
    if (url == null)
      url = fileAsURL(new File(this.file(), _name + ".wod"));
    return url;
  }

  protected static ConcurrentHashMap<IOFSFileInfo, TemplateCacheEntry>
    fileInfoToTemplateEntry =
      new ConcurrentHashMap<IOFSFileInfo, TemplateCacheEntry>(64);
  protected static ConcurrentHashMap<IOFSFileInfo, ScriptCacheEntry>
    fileInfoToScriptEntry =
      new ConcurrentHashMap<IOFSFileInfo, ScriptCacheEntry>(64);
  
  public WOTemplate loadTemplate
    (final String _name, final WOResourceManager _rm)
  {
    /* Note: The cache entry key is the path of this .wo wrapper. But the file
     *       info contents of the wrapper are not relevant for the validity of
     *       a cache entry.
     */
    // TBD: we might want to include languages in the name/lookup
    final IOFSFileInfo info = this.fileInfo();
    TemplateCacheEntry cacheEntry = fileInfoToTemplateEntry.get(info);
    
    long currentHtmlTimestamp = 0;
    long currentWodTimestamp  = 0;
    File baseName = null;
    File wodFile  = null;
    
    if (cacheEntry != null) {
      currentHtmlTimestamp = cacheEntry.htmlFile.lastModified();
      if (currentHtmlTimestamp != cacheEntry.htmlTimestamp)
        cacheEntry = null; /* did change */

      /* If the file disappeared, the new timestamp will be 0, hence won't match
       * and we will reread. Note that you can have just ONE of the two
       * variants, if the 'other' file isn't deleted, it will be used.
       * But what if it didn't exist before? We support two different names.
       */
    }
    if (cacheEntry != null) {
      if (cacheEntry.wodFile != null) {
        /* there was a .wod file */
        currentWodTimestamp = cacheEntry.wodFile.lastModified();
        if (currentWodTimestamp != cacheEntry.wodTimestamp)
          cacheEntry = null; /* did change */
      }
      else {
        /* there was no .wod file */
        if (baseName == null) baseName = this.file();
        
        wodFile = new File(baseName, _name + ".wod");
        if (wodFile.length() > 0)
          cacheEntry = null;
        else {
          wodFile = new File(baseName, "Component.wod");
          if (wodFile.length() > 0)
            cacheEntry = null;
          else
            wodFile = null;
        }
      }
    }
    
    if (cacheEntry != null) {
      // System.err.println("CACHE HIT!");
      return cacheEntry.template;
    }
    
    /* cache miss, build template */
    // System.err.println("CACHE MISS.");
    
    if (baseName == null) baseName = this.file();
    
    /* locate HTML file */
    
    File htmlFile = new File(baseName, _name + ".html"); /* eg Main.html */
    if ((currentHtmlTimestamp = htmlFile.lastModified()) == 0) {
      htmlFile = new File(baseName, "Component.html");
      if ((currentHtmlTimestamp = htmlFile.lastModified()) == 0) {
        /* we always need an .html file to produce a template */
        // TBD: cache misses?
        return null;
      }
    }
    URL htmlURL = fileAsURL(htmlFile);
    
    /* locate WOD file, not having one is OK (inline bindings or plain HTML) */
    
    if (wodFile == null) {
      wodFile = new File(baseName, _name + ".wod"); /* eg Main.wod */
      if (!wodFile.exists()) {
        wodFile = new File(baseName, "Component.wod");
        if (!wodFile.exists())
          wodFile = null;
      }
    }
    URL wodURL = wodFile != null ? fileAsURL(wodFile) : null;
    
    /* build template */
    
    WOWrapperTemplateBuilder builder = new WOWrapperTemplateBuilder();
    WOTemplate tmpl = builder.buildTemplate(htmlURL, wodURL, _rm);
    
    /* cache */
    
    cacheEntry = new TemplateCacheEntry();
    cacheEntry.htmlFile      = htmlFile;
    cacheEntry.htmlTimestamp = currentHtmlTimestamp;
    cacheEntry.wodFile       = wodFile;
    cacheEntry.wodTimestamp  = currentWodTimestamp;
    cacheEntry.template      = tmpl;
    fileInfoToTemplateEntry.put(info, cacheEntry);
    
    /* done */
    return tmpl;
  }
  
  public Script loadScript(String _name) {
    final IOFSFileInfo info = this.fileInfo();
    
    /* check cache */
    
    long currentScriptTimestamp = 0;
    File scriptFile = null;
    File baseName = null;
    
    ScriptCacheEntry cacheEntry = fileInfoToScriptEntry.get(info);
    if (cacheEntry != null) {
      if (cacheEntry.scriptFile != null) {
        /* there was a .js file */
        currentScriptTimestamp = cacheEntry.scriptFile.lastModified();
        if (currentScriptTimestamp != cacheEntry.scriptTimestamp)
          cacheEntry = null; /* did change */
      }
      else {
        /* there was no .wod file */
        if (baseName == null) baseName = this.file();
        
        scriptFile = new File(baseName, _name + ".js");
        if (scriptFile.length() > 0)
          cacheEntry = null;
        else {
          scriptFile = new File(baseName, "Component.js");
          if (scriptFile.length() > 0)
            cacheEntry = null;
          else
            scriptFile = null;
        }
      }
    }
    
    if (cacheEntry != null) {
      //System.err.println("SCRIPT CACHE HIT!");
      return cacheEntry.script;
    }
    
    /* cache miss, build template */
    //System.err.println("SCRIPT CACHE MISS.");
    
    if (baseName == null) baseName = this.file();

    /* locate script file, not having one is OK */
    
    if (scriptFile == null) {
      scriptFile = new File(baseName, _name + ".js"); /* eg Main.js */
      if ((currentScriptTimestamp = scriptFile.lastModified()) == 0) {
        scriptFile = new File(baseName, "Component.js");
        if ((currentScriptTimestamp = scriptFile.lastModified()) == 0)
          scriptFile = null;
      }
    }
    
    /* compile script */
    
    Reader in = null;
    if (scriptFile != null) {
      try {
        in = new FileReader(scriptFile);
      }
      catch (FileNotFoundException e) {
        scriptFile = null;
      }
    }
    
    Script script = null;
    if (in != null) {
      Context jscx = Context.getCurrentContext();
      try {
        script = jscx.compileReader
          (in, scriptFile.getAbsolutePath(), 1 /* line */, null /* security */);
      }
      catch (IOException e) {
        log.error("could not read JS script: " + scriptFile, e);
      }
    }
    
    /* cache */
    
    cacheEntry = new ScriptCacheEntry();
    cacheEntry.scriptFile      = scriptFile;
    cacheEntry.scriptTimestamp = currentScriptTimestamp;
    cacheEntry.script          = script;
    fileInfoToScriptEntry.put(info, cacheEntry);
    
    /* done */
    return script;
  }
  
  @Override
  public WOComponentDefinition definitionForComponent
    (final String _name, final String[] _langs, final WOResourceManager _rm)
  {
    if (this.cdef != null)
      return this.cdef;
    
    // TBD: should we make WOComponentDefinition an interface?!
    // TBD: we might support some config, eg to specify a DIFFERENT root
    //      class (not JSComponent)

    /* locate Script */

    // its not strictly necessary that the component has a script
    File jsData = new File(this.file(), "Component.js");
    if (jsData == null || !jsData.isFile()) {
      jsData = new File(this.file(), _name + ".js");
      if (jsData != null && !jsData.isFile())
        jsData = null;
    }
    
    /* def */

    this.cdef = new JSComponentDefinition
      (_name, this.lookupComponentClass(_name, _rm));
    
    this.cdef.setTemplate(this.loadTemplate(_name, _rm));
    ((JSComponentDefinition)this.cdef).setScript(this.loadScript(_name));

    return this.cdef;
  }
  
  private static URL fileAsURL(File _f) {
    if (_f == null)
      return null;
    if (!_f.isFile())
      return null;
    try { return _f.toURL(); }
    catch (MalformedURLException e) { };
    return null;
  }
  
  
  /* template cache */
  
  public static class TemplateCacheEntry extends Object {
    public WOTemplate template;
    public File htmlFile;
    public long htmlTimestamp;
    public File wodFile;
    public long wodTimestamp;
  }
  public static class ScriptCacheEntry extends Object {
    public File   scriptFile;
    public long   scriptTimestamp;
    public Script script;
    // TBD: could we eval the script against a scope which we then use as the
    //      prototype of the component? Probably 'var' declared variables would
    //      refer to the (shared) prototype scope? (can be fixed with 'dynamic'
    //      scopes?)
  }
}
