/*
 * Copyright (c) 1998-2010 Caucho Technology -- all rights reserved
 * Copyright (c) 2011-2012 Clever Cloud SAS -- all rights reserved
 *
 * This file is part of Bianca(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Bianca Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Bianca Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Bianca Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Sam
 * @author Marc-Antoine Perennou <Marc-Antoine@Perennou.com>
 */
package com.clevercloud.bianca.module;

import com.clevercloud.bianca.BiancaContext;
import com.clevercloud.bianca.env.*;
import com.clevercloud.util.L10N;

import java.util.HashMap;

public class IniDefinition {

   private L10N L = new L10N(IniDefinition.class);
   public static final int PHP_INI_USER = 1;
   public static final int PHP_INI_PERDIR = 2;
   public static final int PHP_INI_SYSTEM = 4;
   public static final int PHP_INI_ALL = 7;
   public static IniDefinition NULL = new Null();
   private final String _name;
   private final int _scope;
   private final Value _deflt;
   private final Type _type;

   public enum Type {

      BOOLEAN, STRING, LONG
   }

   ;

   public IniDefinition(String name, Type type, Value deflt, int scope) {
      assert name != null;

      assert deflt != null;

      assert scope == PHP_INI_USER
         || scope == PHP_INI_PERDIR
         || scope == PHP_INI_SYSTEM
         || scope == PHP_INI_ALL;

      _name = name.intern();
      _type = type;
      _scope = scope;
      _deflt = deflt;
   }

   /**
    * Returns the name of the ini definition.
    */
   protected String getName() {
      return _name;
   }

   /**
    * Returns the default value of the ini definition.
    */
   protected Value getDefault() {
      return _deflt;
   }

   /**
    * Returns the scope in which the value of the ini variable can be set.
    */
   public int getScope() {
      return _scope;
   }

   /**
    * Returns true if this definition was added at runtime and not
    * definied by a module.
    */
   public boolean isRuntimeDefinition() {
      return false;
   }

   private static BooleanValue toBooleanValue(Value value) {
      if (value instanceof BooleanValue) {
         return (BooleanValue) value;
      }

      if (!(value instanceof StringValue)) {
         return BooleanValue.create(value.toBoolean());
      }

      String valueAsString = value.toString().trim();

      if (valueAsString.length() == 0
         || valueAsString.equalsIgnoreCase("false")
         || valueAsString.equalsIgnoreCase("off")
         || valueAsString.equalsIgnoreCase("0")) {
         return BooleanValue.FALSE;
      } else {
         return BooleanValue.TRUE;
      }
   }

   /*
   private LongValue toLongValue(Value value)
   {
   if (value instanceof LongValue)
   return (LongValue) value;
   else if (! (value instanceof StringValue))
   return LongValue.create(value.toLong());

   String valueAsString = value.toString().trim();

   if (valueAsString.length() == 0)
   return LongValue.ZERO;

   char suffix = valueAsString.charAt(valueAsString.length() - 1);

   long val = value.toLong();

   if (suffix == 'G')
   val = 1024 * 1024 * val;
   else if (suffix == 'M')
   val = 1024 * 1024 * val;
   else if (suffix == 'K')
   val = 1024 * val;

   return LongValue.create(val);
   }
    */

   /**
    * Set the ini value for the given scope.
    */
   public void set(BiancaContext bianca, Value value) {
      set(bianca.getIniMap(true), PHP_INI_SYSTEM, value);
   }

   /**
    * Set the ini value for the given scope.
    */
   public void set(BiancaContext bianca, String value) {
      set(bianca, new StringValue(value));
   }

   /**
    * Set the ini value for the given scope.
    */
   public void set(Env env, Value value) {
      set(env.getIniMap(true), PHP_INI_USER, value);
   }

   /**
    * Set the ini value for the given scope.
    */
   public void set(Env env, String value) {
      set(env.getIniMap(true),
         PHP_INI_USER,
         env.createString(value));
   }

   /**
    * Set the ini value for the given scope.
    */
   protected void set(HashMap<String, Value> map,
                      int scope,
                      Value value) {
      if (scope == PHP_INI_USER
         && !(_scope == PHP_INI_USER || _scope == PHP_INI_ALL)) {
         // do nothing
      } else if (_type == Type.BOOLEAN) {
         map.put(_name, toBooleanValue(value));
      } /* up to modules to interpret values
       * http://bugs.clevercloud.com/view.php?id=2095
      else if (_type == Type.LONG)
      map.put(_name, toLongValue(value));
       */ else {
         map.put(_name, value);
      }
   }

   private Value get(HashMap<String, Value> envMap,
                     HashMap<String, Value> biancaMap) {
      Value value = null;

      if (envMap != null) {
         value = envMap.get(_name);
      }

      if (value == null && biancaMap != null) {
         value = biancaMap.get(_name);
      }

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

      if (value == null) {
         value = NullValue.NULL;
      }

      return value;
   }

   /**
    * Returns the value set for name, or the default from the definition if
    * it has not been set.
    */
   public Value getValue(BiancaContext bianca) {
      return get(null, bianca.getIniMap(false));
   }

   /**
    * Returns the value set for name, the first oof the local value set for the
    * environment, the global configuration value set for bianca, or the default
    * from the definition.
    *
    * @returns the value, or NullValue.NULL.
    */
   public Value getValue(Env env) {
      return get(env.getIniMap(false), env.getBianca().getIniMap(false));
   }

   public StringValue getAsStringValue(BiancaContext bianca) {
      return get(null, bianca.getIniMap(false)).toStringValue();
   }

   public StringValue getAsStringValue(Env env) {
      return getValue(env).toStringValue();
   }

   /**
    * @returns the value, null if the value is empty
    */
   public String getAsString(Env env) {
      StringValue value = getAsStringValue(env);

      return (value.length() == 0) ? null : value.toString();
   }

   public boolean getAsBoolean(BiancaContext bianca) {
      return getAsBooleanValue(bianca).toBoolean();
   }

   public boolean getAsBoolean(Env env) {
      return getAsBooleanValue(env).toBoolean();
   }

   public BooleanValue getAsBooleanValue(BiancaContext bianca) {
      return getAsBooleanValue(null, bianca.getIniMap(false));
   }

   public BooleanValue getAsBooleanValue(Env env) {
      return getAsBooleanValue(env.getIniMap(false),
         env.getBianca().getIniMap(false));
   }

   private BooleanValue getAsBooleanValue(HashMap<String, Value> overrideMap,
                                          HashMap<String, Value> iniMap) {
      Value value = get(overrideMap, iniMap);

      return toBooleanValue(value);
   }

   public LongValue getAsLongValue(BiancaContext bianca) {
      return getAsLongValue(null, bianca.getIniMap(false));
   }

   public LongValue getAsLongValue(Env env) {
      return getAsLongValue(env.getIniMap(false),
         env.getBianca().getIniMap(false));
   }

   private LongValue getAsLongValue(HashMap<String, Value> overrideMap,
                                    HashMap<String, Value> iniMap) {
      Value value = get(overrideMap, iniMap);

      if (value instanceof LongValue) {
         return (LongValue) value;
      } else if (value instanceof BooleanValue) {
         return value.toBoolean() ? LongValue.ONE : LongValue.ZERO;
      } else {
         return LongValue.create(value.toLong());
      }
   }

   public long getAsLong(Env env) {
      return getAsLongValue(env).toLong();
   }

   public long getAsLongBytes(Env env, long deflt) {
      String bytes = getAsString(env);

      if (bytes == null) {
         return deflt;
      }

      long value = 0;
      long sign = 1;
      int i = 0;
      int length = bytes.length();

      if (length == 0) {
         return deflt;
      }

      if (bytes.charAt(i) == '-') {
         sign = -1;
         i++;
      } else if (bytes.charAt(i) == '+') {
         i++;
      }

      if (length <= i) {
         return deflt;
      }

      int ch;
      for (; i < length && (ch = bytes.charAt(i)) >= '0' && ch <= '9'; i++) {
         value = 10 * value + ch - '0';
      }

      value = sign * value;

      if (bytes.endsWith("gb") || bytes.endsWith("g") || bytes.endsWith("G")) {
         return value * 1024L * 1024L * 1024L;
      } else if (bytes.endsWith("mb")
         || bytes.endsWith("m")
         || bytes.endsWith("M")) {
         return value * 1024L * 1024L;
      } else if (bytes.endsWith("kb")
         || bytes.endsWith("k")
         || bytes.endsWith("K")) {
         return value * 1024L;
      } else if (bytes.endsWith("b") || bytes.endsWith("B")) {
         return value;
      } else if (value < 0) {
         return value;
      } else {
         return value;

//      env.warning(L.l(
//        "byte-valued expression '{0}' for ini value '{1}' must have units.  "
//        + "'16B' for bytes, '16K' for kilobytes, '16M' for megabytes, "
//        + "'16G' for gigabytes",
//        _name));
         //return deflt;
      }
   }

   static public class Unsupported
      extends IniDefinition {

      private L10N L = new L10N(Unsupported.class);

      public Unsupported(String name, Type type, Value deflt, int scope) {
         super(name, type, deflt, scope);
      }

      @Override
      public void set(HashMap<String, Value> map,
                      int scope,
                      Value value) {
         Env env = Env.getInstance();

         // php/1a17
         // TODO: Env may not be around yet, so need another way to notify user
         //      of unsupported options
         if (env == null) {
            return;
         }

         // php/1a0u
         // don't send a warning if it's being turned off (for Drupal)
         // TODO: send notice instead?
         if (toBooleanValue(value).equals(toBooleanValue(getDefault()))) {
            return;
         } //      Env.getInstance()
         //        .notice(L.l("ini value `{0}' is not supported", getName()));
         else {
            env.warning(L.l("ini value `{0}' is not supported", getName()));
         }
      }
   }

   static private class Null
      extends Unsupported {

      private L10N L = new L10N(Unsupported.class);

      public Null() {
         super("#null", IniDefinition.Type.STRING, NullValue.NULL, PHP_INI_ALL);
      }

      @Override
      public void set(HashMap<String, Value> map,
                      int scope,
                      Value value) {
         if (true) {
            throw new UnsupportedOperationException();
         }

         Env.getInstance().warning(L.l("ini value `{0}' is not supported", getName()));
      }
   }

   static public class Runtime
      extends IniDefinition {

      public Runtime(String name) {
         super(name,
            IniDefinition.Type.STRING,
            NullValue.NULL,
            IniDefinition.PHP_INI_ALL);
      }

      @Override
      public boolean isRuntimeDefinition() {
         return true;
      }
   }
}
