/*
 * Copyright 1999,2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.modeshape.webdav.locking;

import java.util.UUID;
import org.modeshape.common.i18n.TextI18n;
import org.modeshape.common.logging.Logger;

/**
 * a helper class for ResourceLocks, represents the Locks
 * 
 * @author re
 */
public class LockedObject {

    private static final Logger LOGGER = Logger.getLogger(LockedObject.class);

    private ResourceLocks resourceLocks;

    private String path;

    private String id;

    /**
     * Describing the depth of a locked collection. If the locked resource is not a collection, depth is 0 / doesn't matter.
     */
    protected int lockDepth;

    /**
     * Describing the timeout of a locked object (ms)
     */
    protected long expiresAt;

    /**
     * owner of the lock. shared locks can have multiple owners. is null if no owner is present
     */
    protected String[] owner = null;

    /**
     * children of that lock
     */
    protected LockedObject[] children = null;

    protected LockedObject parent = null;

    /**
     * weather the lock is exclusive or not. if owner=null the exclusive value doesn't matter
     */
    protected boolean exclusive = false;

    /**
     * weather the lock is a write or read lock
     */
    protected String type = null;

    /**
     * @param resLocks the resourceLocks where locks are stored
     * @param path the path to the locked object
     * @param temporary indicates if the LockedObject should be temporary or not
     */
    public LockedObject( ResourceLocks resLocks,
                         String path,
                         boolean temporary ) {
        this.path = path;
        id = UUID.randomUUID().toString();
        resourceLocks = resLocks;

        if (!temporary) {
            resourceLocks.locks.put(path, this);
            resourceLocks.locksByID.put(id, this);
        } else {
            resourceLocks.tempLocks.put(path, this);
            resourceLocks.tempLocksByID.put(id, this);
        }
        resourceLocks.cleanupCounter++;
    }

    /**
     * adds a new owner to a lock
     * 
     * @param owner string that represents the owner
     * @return true if the owner was added, false otherwise
     */
    public boolean addLockedObjectOwner( String owner ) {

        if (this.owner == null) {
            this.owner = new String[1];
        } else {

            int size = this.owner.length;
            String[] newLockObjectOwner = new String[size + 1];

            // check if the owner is already here (that should actually not
            // happen)
            for (int i = 0; i < size; i++) {
                if (this.owner[i].equals(owner)) {
                    return false;
                }
            }

            System.arraycopy(this.owner, 0, newLockObjectOwner, 0, size);
            this.owner = newLockObjectOwner;
        }

        this.owner[this.owner.length - 1] = owner;
        return true;
    }

    /**
     * tries to remove the owner from the lock
     * 
     * @param owner string that represents the owner
     */
    public void removeLockedObjectOwner( String owner ) {

        try {
            if (this.owner != null) {
                int size = this.owner.length;
                for (int i = 0; i < size; i++) {
                    // check every owner if it is the requested one
                    if (this.owner[i].equals(owner)) {
                        // remove the owner
                        size -= 1;
                        String[] newLockedObjectOwner = new String[size];
                        for (int j = 0; j < size; j++) {
                            if (j < i) {
                                newLockedObjectOwner[j] = this.owner[j];
                            } else {
                                newLockedObjectOwner[j] = this.owner[j + 1];
                            }
                        }
                        this.owner = newLockedObjectOwner;

                    }
                }
                if (this.owner.length == 0) {
                    this.owner = null;
                }
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            LOGGER.error(e, new TextI18n("LockedObject.removeLockedObjectOwner()"));
        }
    }

    /**
     * adds a new child lock to this lock
     * 
     * @param newChild new child
     */
    public void addChild( LockedObject newChild ) {
        if (children == null) {
            children = new LockedObject[0];
        }
        int size = children.length;
        LockedObject[] newChildren = new LockedObject[size + 1];
        System.arraycopy(children, 0, newChildren, 0, size);
        newChildren[size] = newChild;
        children = newChildren;
    }

    /**
     * deletes this Lock object. assumes that it has no children and no owners (does not check this itself)
     */
    public void removeLockedObject() {
        if (this != resourceLocks.root && !this.getPath().equals("/")) {

            int size = parent.children.length;
            for (int i = 0; i < size; i++) {
                if (parent.children[i].equals(this)) {
                    LockedObject[] newChildren = new LockedObject[size - 1];
                    for (int i2 = 0; i2 < (size - 1); i2++) {
                        if (i2 < i) {
                            newChildren[i2] = parent.children[i2];
                        } else {
                            newChildren[i2] = parent.children[i2 + 1];
                        }
                    }
                    if (newChildren.length != 0) {
                        parent.children = newChildren;
                    } else {
                        parent.children = null;
                    }
                    break;
                }
            }

            // removing from hashtable
            resourceLocks.locksByID.remove(getID());
            resourceLocks.locks.remove(getPath());

            // now the garbage collector has some work to do
        }
    }

    /**
     * deletes this Lock object. assumes that it has no children and no owners (does not check this itself)
     */
    public void removeTempLockedObject() {
        if (this != resourceLocks.tempRoot) {
            // removing from tree
            if (parent != null && parent.children != null) {
                int size = parent.children.length;
                for (int i = 0; i < size; i++) {
                    if (parent.children[i].equals(this)) {
                        LockedObject[] newChildren = new LockedObject[size - 1];
                        for (int i2 = 0; i2 < (size - 1); i2++) {
                            if (i2 < i) {
                                newChildren[i2] = parent.children[i2];
                            } else {
                                newChildren[i2] = parent.children[i2 + 1];
                            }
                        }
                        if (newChildren.length != 0) {
                            parent.children = newChildren;
                        } else {
                            parent.children = null;
                        }
                        break;
                    }
                }

                // removing from hashtable
                resourceLocks.tempLocksByID.remove(getID());
                resourceLocks.tempLocks.remove(getPath());

                // now the garbage collector has some work to do
            }
        }
    }

    /**
     * checks if a lock of the given exclusivity can be placed, only considering children up to "depth"
     * 
     * @param exclusive wheather the new lock should be exclusive
     * @param depth the depth to which should be checked
     * @return true if the lock can be placed
     */
    public boolean checkLocks( boolean exclusive,
                               int depth ) {
        if (checkParents(exclusive) && checkChildren(exclusive, depth)) {
            return true;
        }
        return false;
    }

    /**
     * helper of checkLocks(). looks if the parents are locked
     * 
     * @param exclusive wheather the new lock should be exclusive
     * @return true if no locks at the parent path are forbidding a new lock
     */
    private boolean checkParents( boolean exclusive ) {
        if (path.equals("/")) {
            return true;
        }
        if (owner == null) {
            // no owner, checking parents
            return parent != null && parent.checkParents(exclusive);
        }
        // there already is a owner
        return !(this.exclusive || exclusive) && parent.checkParents(exclusive);
    }

    /**
     * helper of checkLocks(). looks if the children are locked
     * 
     * @param exclusive whether the new lock should be exclusive
     * @param depth depth
     * @return true if no locks at the children paths are forbidding a new lock
     */
    private boolean checkChildren( boolean exclusive,
                                   int depth ) {
        if (children == null) {
            // a file

            return owner == null || !(this.exclusive || exclusive);
        }
        // a folder

        if (owner == null) {
            // no owner, checking children

            if (depth != 0) {
                boolean canLock = true;
                int limit = children.length;
                for (int i = 0; i < limit; i++) {
                    if (!children[i].checkChildren(exclusive, depth - 1)) {
                        canLock = false;
                    }
                }
                return canLock;
            }
            // depth == 0 -> we don't care for children
            return true;
        }
        // there already is a owner
        return !(this.exclusive || exclusive);

    }

    /**
     * Sets a new timeout for the LockedObject
     * 
     * @param timeout
     */
    public void refreshTimeout( int timeout ) {
        expiresAt = System.currentTimeMillis() + (timeout * 1000);
    }

    /**
     * Gets the timeout for the LockedObject
     * 
     * @return timeout
     */
    public long getTimeoutMillis() {
        return (expiresAt - System.currentTimeMillis());
    }

    /**
     * Return true if the lock has expired.
     * 
     * @return true if timeout has passed
     */
    public boolean hasExpired() {
        if (expiresAt != 0) {
            return (System.currentTimeMillis() > expiresAt);
        }
        return true;
    }

    /**
     * Gets the LockID (locktoken) for the LockedObject
     * 
     * @return locktoken
     */
    public String getID() {
        return id;
    }

    /**
     * Gets the owners for the LockedObject
     * 
     * @return owners
     */
    public String[] getOwner() {
        return owner;
    }

    /**
     * Gets the path for the LockedObject
     * 
     * @return path
     */
    public String getPath() {
        return path;
    }

    /**
     * Sets the exclusivity for the LockedObject
     * 
     * @param exclusive
     */
    public void setExclusive( boolean exclusive ) {
        this.exclusive = exclusive;
    }

    /**
     * Gets the exclusivity for the LockedObject
     * 
     * @return exclusivity
     */
    public boolean isExclusive() {
        return exclusive;
    }

    /**
     * Gets the exclusivity for the LockedObject
     * 
     * @return exclusivity
     */
    public boolean isShared() {
        return !exclusive;
    }

    /**
     * Gets the type of the lock
     * 
     * @return type
     */
    public String getType() {
        return type;
    }

    /**
     * Gets the depth of the lock
     * 
     * @return depth
     */
    public int getLockDepth() {
        return lockDepth;
    }

}
