/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin 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.
 *
 * Resin 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 Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.vfs;

import com.caucho.util.Alarm;
import com.caucho.util.ByteBuffer;
import com.caucho.util.L10N;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;

public class MemoryPath extends FilesystemPath {
  private static L10N L = new L10N(MemoryPath.class);

  private Node _rootNode;

  protected MemoryPath(FilesystemPath root,
                       String userPath, Map<String,Object> attributes,
                       String path)
  {
    super(root, userPath, path);

    if (root instanceof MemoryPath)
      _rootNode = ((MemoryPath) root)._rootNode;
    else
      _root = this;
  }

  public MemoryPath()
  {
    this(null, "/", null, "/");

    _root = this;
    _rootNode = new Node("", null, Node.DIR);
  }

  public Path fsWalk(String userPath,
                     Map<String,Object> attributes,
                     String path)
  {
    return new MemoryPath(_root, userPath, attributes, path);
  }

  public String getScheme()
  {
    return "memory";
  }

  public String getURL()
  {
    return getScheme() + ":" + getFullPath();
  }

  @Override
  public boolean isPathCacheable()
  {
    return false;
  }

  public boolean exists()
  {
    synchronized (_rootNode) {
      return lookupAll() != null;
    }
  }

  private Node lookupAll()
  {
    String fullPath = getFullPath();

    int head = 0;
    Node node = _rootNode;

    while (node != null && head < fullPath.length()) {
      int tail = fullPath.indexOf('/', head);

      if (tail == -1) {
	if (head < fullPath.length())
	  return node.lookup(fullPath.substring(head));
	else
	  return node;
      }

      if (head != tail)
        node = node.lookup(fullPath.substring(head, tail));

      head = tail + 1;
    }

    return node;
  }

  private Node lookupAllButTail()
  {
    String fullPath = getFullPath();

    int head = 0;
    Node node = this._rootNode;

    while (node != null && head < fullPath.length()) {
      int tail = fullPath.indexOf('/', head);

      if (tail == -1 || tail == fullPath.length() - 1)
        return node;

      if (head != tail)
        node = node.lookup(fullPath.substring(head, tail));

      head = tail + 1;
    }

    return node;
  }

  public boolean isDirectory()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      return node != null && node.type == node.DIR;
    }
  }

  public boolean isFile()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      return node != null && node.type == node.FILE;
    }
  }

  public boolean isObject()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      return node != null && node.type == node.OBJECT;
    }
  }

  public boolean setExecutable(boolean isExecutable)
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      if (node != null && (node.type == node.FILE || node.type == node.DIR)) {
        node.isExecutable = isExecutable;
        return true;
      }
      else
        return false;
    }
  }

  public boolean isExecutable()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      if (node != null && (node.type == node.FILE || node.type == node.DIR))
        return node.isExecutable;
      else
        return false;
    }
  }

  public long getLength()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      if (node != null && node.type == node.FILE)
        return ((ByteBuffer) node.data).length();
      else
        return 0;
    }
  }

  public long getLastModified()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      return node == null ? 0 : node.lastModified;
    }
  }

  public boolean canRead()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      return node != null;
    }
  }

  public boolean canWrite()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      return node != null;
    }
  }

  public String []list()
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      if (node == null || node.data != null)
        return new String[0];

      ArrayList<String> a = new ArrayList<String>();
      for (Node child = node.firstChild; child != null; child = child.next) {
        a.add(child.name);
      }

      return (String []) a.toArray(new String[a.size()]);
    }
  }

  // XXX: could have iterator

  private boolean mkdir(boolean parent)
  {
    synchronized (_rootNode) {
      String fullPath = getFullPath();

      int head = 0;
      Node node = this._rootNode;

      while (node != null && head < fullPath.length()) {
        int tail = fullPath.indexOf('/', head);
        String name;

        if (tail == head) {
          head = tail + 1;
          continue;
        }
        if (tail == -1) {
          name = fullPath.substring(head);
          if (node.lookup(name) != null)
            return false;
          node.createDir(name);
          return true;
        }

        name = fullPath.substring(head, tail);
        Node next = node.lookup(name);

        if (next == null && parent)
          next = node.createDir(name);

        if (next == null || next.type != next.DIR)
          return false;

        node = next;
        head = tail + 1;
      }

      return false;
    }
  }

  public boolean mkdir()
  {
    return mkdir(false);
  }

  public boolean mkdirs()
  {
    return mkdir(true);
  }

  public boolean remove()
  {
    synchronized (_rootNode) {
      Node node = lookupAllButTail();
      String tail = getTail();

      if (node == null)
        return false;

      Node child = node.lookup(tail);
      if (child == null || child.firstChild != null)
        return false;

      node.remove(tail);

      return true;
    }
  }

  public boolean renameTo(Path path)
  {
    synchronized (_rootNode) {
      if (! (path instanceof MemoryPath))
        return false;

      MemoryPath file = (MemoryPath) path;
      if (_rootNode != file._rootNode)
        return false;

      Node oldParent = lookupAllButTail();
      if (oldParent == null)
        return false;

      Node child = oldParent.lookup(getTail());
      if (child == null)
        return false;

      Node newParent = file.lookupAllButTail();
      if (newParent == null || newParent.type != Node.DIR)
        return false;

      if (newParent.lookup(file.getTail()) != null)
        return false;

      oldParent.remove(getTail());
      child.name = file.getTail();
      newParent.create(child);

      return true;
    }
  }

  public StreamImpl openReadImpl() throws IOException
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      if (node == null)
        throw new FileNotFoundException(getPath());
      else if (node.type != node.FILE)
        throw new IOException("is directory: " + getPath());

      return new MemoryStream(node, (ByteBuffer) node.data, false);
    }
  }

  public StreamImpl openWriteImpl() throws IOException
  {
    return openWriteImpl(false);
  }

  public StreamImpl openAppendImpl() throws IOException
  {
    return openWriteImpl(true);
  }

  private StreamImpl openWriteImpl(boolean append)
    throws IOException
  {
    synchronized (_rootNode) {
      Node node = lookupAllButTail();
      String tail = getTail();

      if (node == null || node.type != Node.DIR)
        throw new IOException(L.l("can't create file {0}", getFullPath()));

      Node child = node.lookup(tail);
      if (child == null)
        child = node.createFile(tail, new ByteBuffer(256));
      else if (! append) {
        node.remove(tail);
        child = node.createFile(tail, new ByteBuffer(256));
      }
      else if (child.type != child.FILE)
        throw new IOException(L.l("can't create file {0}", getFullPath()));
      return new MemoryStream(child, (ByteBuffer) child.data, true);
    }
  }

  public Object getValue() throws IOException
  {
    synchronized (_rootNode) {
      Node node = lookupAll();

      if (node == null || node.type != node.OBJECT)
        throw new IOException("no such object: " + getFullPath().toString());

      return node.data;
    }
  }

  synchronized public void setValue(Object object) throws IOException
  {
    synchronized (_rootNode) {
      Node node = lookupAllButTail();
      String tail = getTail();

      if (node == null || node.type != Node.DIR)
        throw new IOException(L.l("can't set object {0}", getFullPath()));

      Node child = node.lookup(tail);
      if (child == null)
        child = node.createObject(tail, object);
      else if (child.type == child.OBJECT)
        child.data = object;
      else
        throw new IOException(L.l("can't set object {0}", getFullPath()));
    }
  }

  public Path copyCache()
  {
    return null;
  }

  public MemoryPath copyDeep()
  {
    MemoryPath path = new MemoryPath();

    path._rootNode = _rootNode.copy();

    return path;
  }

  public boolean equals(Object o)
  {
    if (o == null || ! getClass().equals(o.getClass()))
      return false;

    MemoryPath mp = (MemoryPath) o;

    return getURL().equals(mp.getURL()) && _rootNode == mp._rootNode;
  }

  private static class Node {
    static final int DIR = 0;
    static final int FILE = DIR + 1;
    static final int OBJECT = FILE + 1;

    String name;
    Node next;
    Node firstChild;
    long lastModified;
    int type;
    Object data;
    boolean isExecutable;

    Node(String name, Object data, int type)
    {
      if (name == null)
        throw new NullPointerException();
      this.name = name;
      this.data = data;
      this.type = type;
      this.lastModified = Alarm.getCurrentTime();
    }

    Node lookup(String name)
    {
      for (Node node = firstChild; node != null; node = node.next) {
        if (node.name.equals(name)) {
          return node;
        }
      }

      return null;
    }

    private Node create(String name, Object data, int type)
    {
      for (Node node = firstChild; node != null; node = node.next) {
        if (node.name.equals(name))
          return null;
      }

      Node newNode = new Node(name, data, type);
      newNode.next = firstChild;
      firstChild = newNode;
      lastModified = Alarm.getCurrentTime();

      return newNode;
    }

    Node createDir(String name)
    {
      return create(name, null, DIR);
    }

    Node createFile(String name, ByteBuffer data)
    {
      return create(name, data, FILE);
    }

    Node createObject(String name, Object data)
    {
      return create(name, data, OBJECT);
    }

    Node create(Node newNode)
    {
      newNode.next = firstChild;
      firstChild = newNode;

      return newNode;
    }

    boolean remove(String name)
    {
      Node last = null;
      for (Node node = firstChild; node != null; node = node.next) {
        if (node.name.equals(name)) {
          if (node.firstChild != null)
            return false;

          if (last != null)
            last.next = node.next;
          else
            firstChild = node.next;

          return true;
        }

        last = node;
      }

      return false;
    }

    Node copy()
    {
      Node newNode = new Node(name, data, type);

      if (type == DIR) {
        for (Node child = firstChild; child != null; child = child.next) {
          Node newChild = child.copy();

          newChild.next = newNode.firstChild;
          newNode.firstChild = newChild;
        }
      }

      return newNode;
    }
  };

  public class MemoryStream extends StreamImpl {
    Node _node;
    ByteBuffer _bb;
    int _offset;
    boolean _write;

    MemoryStream(Node node, ByteBuffer bb, boolean write)
    {
      setPath(MemoryPath.this);

      _node = node;
      if (write)
        node.lastModified = Alarm.getCurrentTime();
      _write = write;

      _bb = bb;
    }

    public int getAvailable()
    {
      return _bb.length() - _offset;
    }

    public boolean canRead() { return true; }

    public int read(byte []buf, int bufOffset, int length) throws IOException
    {
      synchronized (_bb) {
        int sublen = _bb.length() - _offset;
        if (length < sublen)
          sublen = length;

        if (sublen <= 0)
          return -1;

        System.arraycopy(_bb.getBuffer(), _offset, buf, bufOffset, sublen);
        _offset += sublen;

        return sublen;
      }
    }

    public int getPosition()
    {
      return _offset;
    }

    public void seekStart(long pos)
    {
      _offset = (int) pos;
      if (_offset < 0)
        _offset = 0;
      if (_offset > _bb.length())
        _offset = _bb.length();
    }

    public boolean canWrite() { return true; }

    /**
     * Writes a buffer to the underlying stream.
     *
     * @param buffer the byte array to write.
     * @param offset the offset into the byte array.
     * @param length the number of bytes to write.
     * @param isEnd true when the write is flushing a close.
     */
    public void write(byte []buf, int offset, int length, boolean isEnd)
      throws IOException
    {
      synchronized (_bb) {
        _bb.add(buf, offset, length);
      }

      _node.lastModified = Alarm.getCurrentTime();
    }
  }
}
