package org.opengroupware.jope.appserver.elements;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.WOAssociation;
import org.opengroupware.jope.appserver.WOContext;
import org.opengroupware.jope.appserver.WODynamicElement;
import org.opengroupware.jope.foundation.NSClassLookupContext;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSObject;

/*
 * WOFormatter
 * 
 * Helper class which deals with formatting attributes of WODynamicElement's.
 * It is based upon java.text.Format.
 * 
 * THREAD: remember that Format objects are usually not thread-safe.
 */
public abstract class WOFormatter extends NSObject {
  protected static final Log log = LogFactory.getLog("WOFormatter");
  
  public static WOFormatter formatterForAssociations
    (Map<String, WOAssociation> _assocs)
  {
    WOAssociation dateformat, numberformat, formatter, formatterClass;
    
    dateformat     = WODynamicElement.grabAssociation(_assocs,"dateformat");
    numberformat   = WODynamicElement.grabAssociation(_assocs,"numberformat");
    formatter      = WODynamicElement.grabAssociation(_assocs,"formatter");
    formatterClass = WODynamicElement.grabAssociation(_assocs,"formatterClass");
    
    // TODO: warn about multiple formatters, OR: wrap multiple formatters in an
    //       compound formatter and return that.

    if (dateformat != null)
      return new WODateFormatter(dateformat);
    
    if (numberformat != null)
      return new WONumberFormatter(numberformat);
    
    if (formatterClass != null)
      return new WOClassFormatter(formatterClass, formatter);
    
    if (formatter != null)
      return new WOObjectFormatter(formatter);
    
    if (log.isInfoEnabled())
      log.info("did not find formatter bindings in given assocs: " + _assocs);
    return null;
  }
  
  protected WOFormatter() {
  }
  
  /* methods */
  
  public abstract Format formatInContext(WOContext _ctx);
  
  /* NSFormatter like wrappers */

  public Object objectValueForString(String _s, WOContext _ctx)
    throws ParseException
  {
    if (_s == null)
      return null;
    
    Format fmt = this.formatInContext(_ctx);
    if (fmt == null)
      return _s;
    
    return fmt.parseObject(_s);
  }
  
  public String stringForObjectValue(Object _o, WOContext _ctx) {
    Format fmt = this.formatInContext(_ctx);
    if (fmt == null)
      return _o != null ? _o.toString() : null;
    
    return fmt.format(_o);
  }
  
  public String editingStringForObjectValue(Object _o, WOContext _ctx) {
    return this.stringForObjectValue(_o, _ctx);
  }

  
  /* implementations */
  
  static class WODateFormatter extends WOFormatter {
    protected WOAssociation format;
    
    /* optimization for constant formats */
    protected String        fmtString;
    protected boolean       isCustomFormat;
    
    public WODateFormatter(WOAssociation _fmt) {
      this.format = _fmt;
      if (_fmt != null) {
        if (_fmt.isValueConstant()) {
          this.fmtString      = _fmt.stringValueInComponent(null /* cursor */);
          this.isCustomFormat = isCustomDateFormat(this.fmtString);
        }
      }
    }
    
    /* creating Format */
    
    protected static final String[] dateStyleFormats = {
      "SHORT", "MEDIUM", "LONG", "FULL",
      "TIME",  "DATE", "DATETIME",
      "TIME.SHORT",     "TIME.MEDIUM",     "TIME.LONG",     "TIME.FULL",
      "DATE.SHORT",     "DATE.MEDIUM",     "DATE.LONG",     "DATE.FULL",
      "DATETIME.SHORT", "DATETIME.MEDIUM", "DATETIME.LONG", "DATETIME.FULL"
    };
    
    protected static boolean isCustomDateFormat(String _fmt) {
      /* this is kinda slow, maybe we should do something else or detect
       * constant dateformats so that we can do it once
       */
      if (_fmt == null || _fmt.length() == 0) return false;
      
      if ("SMLFTD".indexOf(_fmt.charAt(0)) == -1)
        return true;
      
      for (int i = 0; i < dateStyleFormats.length; i++) {
        if (_fmt.equals(dateStyleFormats[i]))
          return false;
      }
      
      return true;
    }
    
    public Format formatInContext(WOContext _ctx) {
      // TODO: we should probably cache some formatter objects?
      // TODO: find out how to do date patterns
      //
      // eg: DateFormat expiresFormat1
      //     = new SimpleDateFormat("E, dd-MMM-yyyy k:m:s 'GMT'", Locale.US)
      String  fmt;
      boolean isCustom;
      
      if (this.fmtString != null) {
        fmt      = this.fmtString;
        isCustom = this.isCustomFormat;
      }
      else {
        fmt = this.format.stringValueInComponent
          (_ctx != null ? _ctx.cursor() : null);

        if (fmt == null)
          return null;
        
        isCustom = isCustomDateFormat(fmt);
      }
      
      Format lFormat;
      
      if (isCustom) {
        lFormat = _ctx != null
          ? new SimpleDateFormat(fmt, _ctx.locale())
          : new SimpleDateFormat(fmt);
      }
      else {
        int mode = DateFormat.SHORT;
        if (fmt.endsWith("SHORT"))       mode = DateFormat.SHORT;
        else if (fmt.endsWith("MEDIUM")) mode = DateFormat.MEDIUM;
        else if (fmt.endsWith("LONG"))   mode = DateFormat.LONG;
        else if (fmt.endsWith("FULL"))   mode = DateFormat.FULL;
        
        if (fmt.startsWith("DATETIME")) {
          lFormat = _ctx != null
            ? DateFormat.getDateTimeInstance(mode, mode, _ctx.locale())
            : DateFormat.getDateTimeInstance(mode, mode);
        }
        else if (fmt.startsWith("TIME")) {
          lFormat = _ctx != null
            ? DateFormat.getTimeInstance(mode, _ctx.locale())
            : DateFormat.getTimeInstance(mode);
        }
        else {
          lFormat = _ctx != null
            ? DateFormat.getDateInstance(mode, _ctx.locale())
            : DateFormat.getDateInstance(mode);
        }
      }
      
      return lFormat;
    }
    
    /* support for Calendar objects */
    
    public String stringForObjectValue(Object _o, WOContext _ctx) {
      if (_o != null) {
        /* convert calendars into dates for formatting */
        if (_o instanceof Calendar)
          _o = ((Calendar)_o).getTime();
      }
      return super.stringForObjectValue(_o, _ctx);
    }
  }
  
  static class WONumberFormatter extends WOFormatter {
    
    protected WOAssociation format = null;
    
    public WONumberFormatter(WOAssociation _fmt) {
      this.format = _fmt;
    }
    
    /* creating Format */
    
    public Format formatInContext(WOContext _ctx) {
      // TODO: we should probably cache some formatter objects?
      // TODO: find out how to do date patterns
      String fmt = this.format.stringValueInComponent(_ctx.cursor());
      
      if (fmt == null)
        return null;
      
      NumberFormat lf = NumberFormat.getInstance(_ctx.locale());
      if (lf == null)
        return null;
      
      if (lf instanceof DecimalFormat)
        ((DecimalFormat)lf).applyPattern(fmt);
      
      return lf;
    }
  }
  
  static class WOObjectFormatter extends WOFormatter {
    
    protected WOAssociation formatter = null;
    
    public WOObjectFormatter(WOAssociation _fmt) {
      this.formatter = _fmt;
    }

    /* creating Format */
    
    public Format formatInContext(WOContext _ctx) {
      if (this.formatter == null)
        return null;
      
      return (Format)this.formatter.valueInComponent(_ctx.cursor());
    }
  }
  
  static class WOClassFormatter extends WOFormatter {
    
    protected WOAssociation formatterClass = null;
    protected WOAssociation format = null;
    
    public WOClassFormatter(WOAssociation _fmt, WOAssociation _val) {
      this.formatterClass = _fmt;
      this.format = _val;
    }

    /* creating Format */
    
    public Format formatInContext(WOContext _ctx) {
      if (this.formatterClass == null)
        return null;
      
      /* determine class */
      
      Object tmp = this.formatterClass.valueInComponent(_ctx.cursor());
      Class  cls = null;
      if (tmp instanceof Class)
        cls = (Class)tmp;
      else {
        NSClassLookupContext clsctx = _ctx.component().resourceManager();
        cls = clsctx.lookupClass(tmp.toString());
        if (cls == null) {
          // TODO: improve logging
          System.err.println("ERROR: did not find formatter: " + tmp);
          return null;
        }
      }
      
      /* instantiate */
      
      if (this.format == null) /* formatter w/o argument */
        return (Format)NSJavaRuntime.NSAllocateObject(cls);
      
      String fmt = this.format.stringValueInComponent(_ctx.cursor());
      return (Format)NSJavaRuntime.NSAllocateObject(cls, String.class, fmt);
    }
  }
}
