/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.jetspeed.util;

import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;

/**
 * <h2>Overview</h2>
 * <p>
 * The Path object is used to standard used to standardize the creation of
 * mutation of path-like structures. For: example /foo/bar/index.html.
 * </p>
 * <h2>Rules for Interperting Pathes</h2>
 * <p>
 * Below are the rules for how the constructor interprets literal paths.
 * <strong>NOTE</strong> the {@link addSegment(String)} interprets string
 * pathes in a somewhat different manner. <table>
 * <tr>
 * <th>Literal Path</th>
 * <th>Interpretation</th>
 * </tr>
 * <td> <i>/foo/bar/index.html</i> </td>
 * <td> <code>foo</code> and <code>bar</code> will be considered directory
 * segments while <code>index.html</code> will be considered a file segment.
 * This means that the <code>baseName</code> will be set to <i>index</i> and
 * the <code>fileExtension</code> will be set to <i>.html</i> </td>
 * <tr>
 * <td> <i>/foo/bar/</i>, <i>/foo/bar</i>, <i>foo/bar/</i> <i>foo/bar</i>
 * </td>
 * <td>
 * <p>
 * <code>foo</code> and <code>bar</code> will be considered directory
 * segments. <code>baseName</code> and <code>fileExtension</code> will be
 * left as <code>null</code>.
 * <p>
 * I cases where a file has no extension you must use the
 * {@link setFileSegment(String))} to manually set the file. This causes the
 * <code>baseName</code> to be set to the file name specified and the
 * <code>fileExtension</code> will be set to the empty string ("").
 * </p>
 * </td>
 * </tr>
 * </table>
 * 
 * @author <href a="mailto:weaver@apache.org">Scott T. Weaver</a>
 */
public class Path implements Serializable, Cloneable
{
    /** The serial version uid. */
    private static final long serialVersionUID = 6890966283704092945L;

    public static final String PATH_SEPERATOR = "/";
    
    private static final String[] EMPTY_SEGMENTS = new String[0];
    
    private static HashMap childrenMap = new HashMap();
    
    private final String path;
    private final String[] segments;

    private final String fileName;

    private final String baseName;

    private final String fileExtension;

    private final String queryString;
    
    private final int hashCode;

    public Path()
    {
        segments = EMPTY_SEGMENTS;
        fileName = null;
        baseName = null;
        fileExtension = null;
        queryString = null;
        hashCode = 0;
        path = "";
    }
    
    private Path(Path parent, String childSegment, boolean pathOnly)
    {
        this(parent, splitPath(childSegment), pathOnly);
    }
            
    private Path(Path parent, String[] children, boolean pathOnly)
    {
        int code = 0;
        if (!pathOnly)
        {
            this.fileName = parent.fileName;
            this.baseName = parent.baseName;
            this.fileExtension = parent.fileExtension;
            this.queryString = parent.queryString;
            if (queryString != null)
            {
                code += queryString.hashCode();
            }
        }
        else
        {
            fileName = null;
            baseName = null;
            fileExtension = null;
            queryString = null;
        }
        
        int size = parent.segments.length;
        if (pathOnly && parent.fileName != null)
        {
            size--;
        }

        int index = 0;
        
        segments = new String[size+children.length];
        for (index = 0; index < size; index++)
        {
            segments[index] = parent.segments[index];
            code += segments[index].hashCode();
        }
        for (int i = 0; i < children.length; i++, index++)
        {
            segments[index] = children[i];
            code += segments[index].hashCode();
        }
        if (fileName != null && !pathOnly)
        {
            segments[index] = fileName;
            code += segments[index].hashCode();
        }
        hashCode = code;
        path = buildPath();
    }
            
    private Path(Path parent)
    {
        this.fileName = parent.fileName;
        this.baseName = parent.baseName;
        this.fileExtension = parent.fileExtension;
        this.queryString = parent.queryString;
        segments = new String[parent.segments.length-1];
        int code = 0;
        for (int i = 0; i < parent.segments.length-2; i++)
        {
            segments[i] = parent.segments[i];
            code += segments.hashCode();
        }
        if (fileName != null)
        {
            segments[segments.length-1] = fileName;
        }
        else if (parent.segments.length > 1)
        {
            segments[segments.length-1] = parent.segments[parent.segments.length-2];
        }
        if ( segments.length > 0)
        {
            code += segments[segments.length-1].hashCode();
        }
        if (queryString != null)
        {
            code += queryString.hashCode();
        }
        hashCode = code;
        path = buildPath();
    }
            
    private Path(String[] segments, int offset, int count)
    {
        this.segments = new String[count];
        int code = 0;
        for (int i = 0; i < count; i++)
        {
            this.segments[i] = segments[offset+i];
            code+=segments[offset+i].hashCode();
        }
        hashCode = code;
        if (count > 0)
        {
            fileName = this.segments[count-1];
            int extIndex = fileName.lastIndexOf('.');
            if (extIndex > -1)
            {
                baseName = fileName.substring(0, extIndex);
                fileExtension = fileName.substring(extIndex);
            }
            else
            {
                baseName = fileName;
                fileExtension = "";
            }
        }
        else
        {
            fileName = null;
            baseName = null;
            fileExtension = null;
        }
        queryString = null;
        path = buildPath();
    }
            
    public Path(String path)
    {
        
        String tmp = path.replace('\\', '/');

        if (!tmp.startsWith("/"))
        {
            tmp = "/" + tmp;
        }

        this.path = tmp;
        
        if (path.equals("/"))
        {
            segments = new String[]{""};
            fileName = null;
            baseName = null;
            fileExtension = null;
            queryString = null;
            hashCode = 0;
        }
        else
        {
            int queryStart = path.indexOf('?');
            int len = queryStart > -1 ? queryStart : path.length();
            segments = split(path, 0, len, '/');
            int code  = 0;
            for (int i = 0; i < segments.length; i++)
            {
                code += segments[i].hashCode();
            }
            String tmpFileName = null;
            
            if (queryStart > 1 && path.length() > queryStart+1)
            {
                queryString = path.substring(queryStart+1);
                code += queryString.hashCode();
            }
            else
            {
                queryString = null;
            }
            hashCode = code;
            int extIndex = -1;
            if (segments.length > 0)
            {
                tmpFileName = segments[segments.length-1];
                extIndex = tmpFileName.lastIndexOf('.');
            }
            if (extIndex > -1)
            {
                fileName = tmpFileName;
                baseName = tmpFileName.substring(0, extIndex);
                fileExtension = tmpFileName.substring(extIndex);
            }
            else
            {
                fileName = null;
                baseName = null;
                fileExtension = null;
            }
        }
    }

    private static String[] splitPath(String path)
    {
        String[] children = null;
        path = path.replace('\\', '/');

        if (path.startsWith("/"))
        {
            path = "/" + path;
        }

        if (path.equals("/"))
        {
            children = new String[]{""};
        }
        else
        {
            int index = path.indexOf('?');
            int len = index > -1 ? index : path.length();
            children = split(path, 0, len, '/');
        }
        return children;
    }
    
    /**
     * Returns the segement of the path at the specified index <code>i</code>.
     * 
     * @param i
     *            index containing the segment to return.
     * @return Segment at index <code>i</code>
     * @throws ArrayIndexOutOfBoundsException
     *             if the index is not within the bounds of this Path.
     */
    public String getSegment(int i)
    {
        return segments[i];
    }

    /**
     * <p>
     * Adds this segment to the end of the path but before the current file
     * segment, if one exists. For consistency Segments added via this method
     * are <strong>ALWAYS</strong> considered directories even when matching a
     * standrad file pattern i.e. <i>index.html</i>
     * </p>
     * <p>
     * If you need to set the file segment, please use the setFileSegment()
     * method.
     * </p>
     * 
     * @param segment
     * @return
     */
    public Path addSegment(String segment)
    {
        return new Path(this, segment, false);
    }

    public Path getSubPath(int beginAtSegment)
    {
        return new Path(segments, beginAtSegment, segments.length-beginAtSegment);
    }

    public Path getSubPath(int beginAtSegment, int endSegment)
    {
        return new Path(segments, beginAtSegment, endSegment-beginAtSegment);
    }

    public String getBaseName()
    {
        return baseName;
    }

    public String getFileExtension()
    {
        return fileExtension;
    }

    public String getFileName()
    {
        return fileName;
    }

    public String getQueryString()
    {
        return queryString;
    }

    public int length()
    {
        return segments.length;
    }
    
    public String toString()
    {
        return path;
    }

    private String buildPath()
    {
        int len = 0;
        for (int i = 0; i < segments.length; i++)
        {
            len+=segments[i].length()+1;
        }
        if (queryString!=null)
        {
            len+=queryString.length()+1;
        }
        char[] buffer = new char[len];
        int index = 0;
        for (int i = 0; i < segments.length; i++ )
        {
            buffer[index++] = '/';
            len = segments[i].length();
            segments[i].getChars(0, len, buffer, index);
            index+= len;
        }
        if (queryString != null)
        {                
            buffer[index++] = '?';
            len = queryString.length();
            queryString.getChars(0, len, buffer, index);
        }
        return new String(buffer);
    }

    public boolean equals(Object obj)
    {
        if (obj instanceof Path)
        {
            Path other = (Path)obj;
            if ( (other.queryString != null && other.queryString.equals(queryString)) ||
                    (other.queryString == null && queryString == null) )
            {
                return Arrays.equals(other.segments,segments);
            }
        }
        return false;
    }

    public int hashCode()
    {
        return hashCode;
    }

    /**
     * Removes the last directory segment in this path. This method <strong>WILL
     * NOT</strong> remove the fileSegment, but path segment immediately before
     * it.
     * 
     * @return segment removed.
     */
    public Path removeLastPathSegment()
    {
        if ((fileName != null && segments.length == 1) || segments.length == 0)
        {
            return this;
        }
        return new Path(this);
    }

    public Path getChild(String childPath)
    {
        synchronized (childrenMap)
        {
            Path child = null;
            HashMap children = (HashMap)childrenMap.get(path);
            if (children == null)
            {
                children = new HashMap();
                childrenMap.put(path, children);
            }
            else
            {
                child = (Path)children.get(childPath);
            }
            if ( child == null )
            {
                if (segments.length == 0)
                {
                    child = new Path(childPath);
                }
                else
                {
                    child = new Path(this, childPath, true);
                }
                children.put(childPath, child);
            }
            return child;
        }
    }

    public Path getChild(Path childPath)
    {
        return getChild(childPath.path);
    }
    
    private static String[] split(String str, int start, int length, char separator)
    {
        String[] result;
        char[] buffer = str.toCharArray();
        int tokens = 0;
        boolean token = false;
        for (int i = start; i < length; i++)
        {
            if (buffer[i]==separator)
            {
                token = false;
            }
            else if (!token)
            {
                tokens++;
                token = true;
            }
        }
        result = new String[tokens];
        if (tokens > 0)
        {
            int begin = start;
            int end = start;
            token = false;
            tokens = 0;
            for (int i = start; i < length; i++)
            {
                if (buffer[i]==separator)
                {
                    if (token)
                    {
                       result[tokens++] = new String(buffer,begin,end);
                       token = false;
                    }
                }
                else if (!token)
                {
                    token = true;
                    begin = i;
                    end = 1;
                }
                else
                {
                    end++;
                }
            }
            if (token)
            {
                result[tokens] = new String(buffer,begin, end);
            }
        }
        return result;
    }
}
