/*
 * Copyright (c) 2003, KNOPFLERFISH project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * - Neither the name of the KNOPFLERFISH project nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.knopflerfish.bundle.prefs;

import org.osgi.framework.*;
import org.osgi.service.prefs.*;
import org.knopflerfish.service.log.LogRef;
import org.knopflerfish.util.Text;
import java.util.*;

import java.io.*;


/**
 *
 *  baseDir
 *    |
 *   nodeDir
 *    |
 *    +-- .keys
 *    |
 *    +-- child
 *    |
 *    +-- child
 *
 *
 */
public class  PrefsStorageFile implements PrefsStorage {
  File baseDir;

  static final String KEYFILE_NAME = ".keys";

  Object lock = new Object();

  /** Set to true when the entire pres tree repesented by this storage
   ** have been removed.
   **/
  boolean bStale = false;

  boolean bDebug = false;

  // User names must be encoded to avoid creating invalid file names
  // Note that specification does not forbid '/' in user names
  PrefsStorageFile(String base, String user) {
    this( base  + "/" + encodeUser(user) );
  }

  PrefsStorageFile(String base) {
    File top = new File(System.getProperty("org.knopflerfish.prefs.dir",
                                           "prefsdir"));
    baseDir  = new File(top, base);
    baseDir.mkdirs();

    if(!baseDir.exists() || !baseDir.isDirectory()) {
      throw new RuntimeException("Failed to create base=" + base);
    }

  }

  File getNodeDir(String path, boolean bCreate) {
    synchronized(lock) {
      if("".equals(path)) {
        // root
      } else {
        if(!path.startsWith("/")) {
          throw new IllegalArgumentException("Path must be absolute, is '" +
                                             path + "'");
        }
        path = path.substring(1);
      }
      File file = new File(baseDir, encode(path));
      if(bCreate) {
        file.mkdirs();
        if(!file.exists() || !file.isDirectory()) {
          throw new RuntimeException("Failed to create node dir=" +
                                     file.getAbsolutePath() + " from path " +
                                     path);
        }
      }
      return file;
    }
  }


  Dictionary loadProps(String path) throws IOException {
    synchronized(lock) {
      Dictionary dict = (Dictionary)propsMap.get(path);
      if(dict != null) {
        return dict;
      }
      Properties props = new Properties();
      InputStream in = null;
      try {
        File f = getKeyFile(path);
        if(f.exists()) {
          in = new FileInputStream(f);
          props.load(in);

          // We might need to decode some keys
          Properties p2 = new Properties();
          for(Enumeration e = props.keys(); e.hasMoreElements(); ) {
            String key        = (String)e.nextElement();
            String decodedKey = decode(key);
            String val        = (String)props.get(key);
            p2.put(decodedKey, val);
          }

          propsMap.put(path, p2);
          return p2;
        } else {
          throw new IllegalStateException("No keys for path=" + path +
                                          ", file=" + f.getAbsolutePath());
        }
      } finally {
        try { in.close(); } catch (Exception ignored) {  }
      }
    }
  }

  void saveProps(String path, Dictionary p) throws IOException {
    synchronized(lock) {

      Properties props = new Properties();
      for(Enumeration e = p.keys(); e.hasMoreElements(); ) {
        String key = (String)e.nextElement();
        Object val = p.get(key);
        props.put(encode(key), val);
      }

      OutputStream out = null;
      try {
        File f = getKeyFile(path);
        out = new FileOutputStream(f);
        props.save(out, "keys for " + path);
      } catch (IOException e) {
        e.printStackTrace();
        throw e;
      } finally {
        try { out.close(); } catch (Exception ignored) {
        }
      }
    }
  }

  public void   put(String path, String key, String val) {
    try {
      dirtySet.add(path);
      Dictionary p = loadProps(path);

      p.put(key, val);

      // System.out.println("dirty " + path);
      //      saveProps(path, p);
    } catch (Exception e) {
      Activator.log.warn("Failed to put path=" + path +
                         ", key=" + key + ", val=" + val,
                         e);
    }
  }



  public String[] getChildrenNames(String path) {
    synchronized(lock) {
      try {
        File dir = getNodeDir(path, false);
        String[] f = dir.list();
        Vector v = new Vector();
        for(int i = 0; i < f.length; i++) {
          if(!f[i].startsWith(".")) {
            v.addElement(decode(f[i]));
          }
        }
        String[] f2 = new String[v.size()];
        v.copyInto(f2);
        return f2;
      } catch (Exception e) {
        throw new IllegalStateException("Failed to get children from '"
                                        + path + "'");
      }
    }
  }

  public String[] getKeys(String path) {
    try {
      Dictionary props = loadProps(path);
      String[]   keys  = new String[props.size()];

      int i = 0;
      for(Enumeration e = props.keys(); e.hasMoreElements(); ) {
        keys[i++] = (String) e.nextElement();
      }
      return keys;
    } catch (Exception e) {
      return new String[0];
    }
  }

  public String get(String path, String key, String def) {
    synchronized(lock) {
      try {
        Dictionary props = loadProps(path);
        String val = (String)props.get(key);

        return val != null ? val : def;

      } catch (IOException e) {
        Activator.log.warn("Failed to read " + path + ", key=" + key);
        return def;
      }
    }
  }


  public void removeAllKeys(String path) {
    synchronized(lock) {
      try {
        Dictionary props = new Hashtable();
        propsMap.put(path, props);
        dirtySet.add(path);
        //        saveProps(path, props);
      } catch (Exception e) {
        Activator.log.warn("Failed to clear " + path);
      }
    }
  }

  public void removeKey(String path, String key) {
    synchronized(lock) {
      try {
        Dictionary props = loadProps(path);
        props.remove(key);
        propsMap.put(path, props);
        dirtySet.add(path);
        //        saveProps(path, props);
      } catch (Exception e) {
        Activator.log.warn("Failed to remove " + path + ", key=" + key);
      }
    }
  }

  public void removeNode(String path) {
    synchronized(lock) {
      try {
        if("".equals(path)) {
          bStale = true;
        }
        
        for(Iterator it = prefs.keySet().iterator(); it.hasNext(); ) {
          String p = (String)it.next();
          PreferencesImpl pi = (PreferencesImpl)prefs.get(p);
          if(bStale || p.startsWith(path + "/")) {
            pi.bStale = true;
          }
        }

        File f = getNodeDir(path, false);
        deleteTree(f);
      } catch (Exception e) {
        e.printStackTrace();
        Activator.log.warn("Failed to remove node " + path);
      }
    }
  }

  // String (path) -> PreferencesImpl
  Map prefs = new HashMap();
  Set dirtySet = new HashSet();
  Map propsMap = new HashMap();

  public Preferences getNode(String path, boolean bCreate) {
    try {
      PreferencesImpl pi = (PreferencesImpl)prefs.get(path);
      if(pi != null) {
        return pi;
      }

      File nodeDir = getNodeDir(path, bCreate);
      File keyFile = getKeyFile(path);

      if(!keyFile.exists()) {
        Dictionary props = new Hashtable();
        propsMap.put(path, props);
        saveProps(path, props);
      }

      pi = new PreferencesImpl(this, path);
      prefs.put(path, pi);
      return pi;
    } catch (Exception e) {
      throw new IllegalStateException("getNode " + path + " failed: " + e);
    }
  }

  public boolean     nodeExists(String path) {
    synchronized(lock) {
      boolean b = false;

      File f = getNodeDir(path, false);
      int ix = path.lastIndexOf('/');

      if(ix != -1 && path.length() > 0) {
        String last  = decode(path.substring(ix + 1));
        String fname = f.getAbsolutePath();

        try {
          fname = f.getCanonicalPath();
        } catch (IOException e) {
          Activator.log.warn("failed to get canonical path of " + path, e);
        }

        fname = decode(fname);
        if(fname.endsWith(last)) {
          b = f.exists();
        }
      } else {
        b = f.exists();
      }
      return b;
    }
  }

  public void flush(String path) {
    synchronized(lock) {
      // save any cached data to storage
      if(path == null) {
        // save all
      } else {
        synchronized(dirtySet) {
          // System.out.println("flushing " + dirtySet.size() + " items");
          for(Iterator it = dirtySet.iterator(); it.hasNext();) {
            String p = (String)it.next();
            Dictionary props = (Dictionary)propsMap.get(p);
            if(props != null) {
              //              System.out.println("flush '" + p + "'");
              try {
                saveProps(p, props);
              } catch (Exception e) {
              }
            }
          }
          dirtySet.clear();
        }
      }
    }
  }

  /**
   * Get file which store key/value pairs
   */
  File getKeyFile(String path) {
    return new File(getNodeDir(path, true), KEYFILE_NAME);
  }

  void deleteTree(File f) {
    synchronized(lock) {
      if(f.exists()) {
        if(f.isDirectory()) {
          String[] children = f.list();
          for(int i = 0; i < children.length; i++) {
            deleteTree(new File(f, children[i]));
          }
        }
        f.delete();
      }
    }
  }

  static String encode(String s) {
    // Must encode '__' since they are used as encoding marker.
    s = Text.replace(s, "__", "__us__");
    s = Text.replace(s, ".", "__dot__");
    s = Text.replace(s, " ", "__space__");
    s = Text.replace(s, "?", "__q__");
    s = Text.replace(s, "\\", "__bslash__");
    return s;
  }

  static String decode(String s) {
    s = Text.replace(s, "__space__",   " ");
    s = Text.replace(s, "__dot__",   ".");
    s = Text.replace(s, "__?__",     "?");
    s = Text.replace(s, "__bslash__", "\\");
    s = Text.replace(s, "__us__", "__");
    return s;
  }

  /** Same encoding as <code>encode()</code> with the addition that
   ** '/' is also encoded.
   ** @param s The string to encode
   ** @return String safe to use as file name.
   **/
  static String encodeUser(String s) {
    s = encode(s);
    s = Text.replace(s, "/", "__slash__");
    return s;
  }

  /** Decodes a string encoded by <code>encodeUser()</code>.
   ** @see #encodeUser(String)
   ** @param s The encoded string to decode
   ** @return String decoded string.
   **/
  static String decodeUser(String s) {
    s = Text.replace(s, "__slash__", "/" );
    s = decode(s);
    return s;
  }

}

