/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.wc;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNExternal {
    
    private SVNRevision myRevision;
    private SVNRevision myPegRevision;
    private String myURL;
    private String myRawURL; //non-canonicalized string used in the property value
    private String myPath;
    private SVNURL myResolvedURL;
    private boolean myIsRevisionExplicit;
    private boolean myIsPegRevisionExplicit;
    private boolean myIsNewFormat;
    private String myRawValue;
    
    private SVNExternal() {
        myRevision = SVNRevision.UNDEFINED;
        myPegRevision = SVNRevision.UNDEFINED;
    }
    
    public SVNExternal(String target, String url, SVNRevision pegRevision, SVNRevision revision, 
            boolean isRevisionExplicit, boolean isPegRevisionExplicit, boolean isNewFormat) {
        myPath = target;
        myURL = url;
        myRawURL = url;
        myRevision = revision;
        myPegRevision = pegRevision;
        myIsRevisionExplicit = isRevisionExplicit;
        myIsPegRevisionExplicit = isPegRevisionExplicit;
        myIsNewFormat = isNewFormat; 
    }
    
    public SVNRevision getRevision() {
        return myRevision;
    }
    
    public SVNRevision getPegRevision() {
        return myPegRevision;
    }
    
    public String getPath() {
        return myPath;
    }

    public String getUnresolvedUrl() {
        return myURL;
    }

    public String getRawValue() {
        return myRawValue;
    }

    //use getUnresolvedUrl() instead; use getRawURL() only if you really know what it returns
    @Deprecated
    protected String getRawURL() {
        return myRawURL;
    }

    public boolean isRevisionExplicit() {
        return myIsRevisionExplicit;
    }
    
    public boolean isPegRevisionExplicit() {
        return myIsPegRevisionExplicit;
    }
    
    public boolean isNewFormat() {
        return myIsNewFormat;
    }

    public int getFormat() {
        return isNewFormat() ? 2 : 1;
    }

    public SVNURL getResolvedURL() {
        return myResolvedURL;
    }

    public SVNURL resolveURL(SVNURL rootURL, SVNURL ownerURL) throws SVNException {
        String canonicalURL;
        if (myURL != null && myURL.startsWith("^/")) {
            canonicalURL = "^/" + SVNPathUtil.canonicalizePath(myURL.substring("^/".length()));
        } else {
            canonicalURL = SVNPathUtil.canonicalizePath(myURL);
        }
        if (SVNPathUtil.isURL(canonicalURL)) {
            myResolvedURL = SVNURL.parseURIEncoded(canonicalURL); 
            return getResolvedURL();
        }
        if (myURL.startsWith("../") || myURL.startsWith("^/")) {
            // ../ relative to the parent directory of the external
            // ^/     relative to the repository root
            String[] base = myURL.startsWith("../") ? ownerURL.getPath().split("/") : rootURL.getPath().split("/");
            LinkedList baseList = new LinkedList(Arrays.asList(base));
            if (canonicalURL.startsWith("^/")) {
                canonicalURL = canonicalURL.substring("^/".length());
            }
            String[] relative = canonicalURL.split("/");
            for (int i = 0; i < relative.length; i++) {
                if ("..".equals(relative[i])) {
                    // remove last from base.
                    if (!baseList.isEmpty()) {
                        baseList.removeLast();
                    }
                } else {
                    baseList.add(relative[i]);
                }
            }
            String finalPath = "/";
            for (Iterator segments = baseList.iterator(); segments.hasNext();) {
                String segment = (String) segments.next();
                finalPath = SVNPathUtil.append(finalPath, segment);
            }
            myResolvedURL = ownerURL.setPath(finalPath, true);
            return getResolvedURL();
        }
        
        if (myURL.indexOf("/../") >= 0 || myURL.startsWith("../") || myURL.endsWith("/..")) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "The external relative URL ''{0}'' cannot have backpaths, i.e. ''..''.", myURL);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        
        if (myURL.startsWith("//")) {
            // //     relative to the scheme
            myResolvedURL = SVNURL.parseURIEncoded(SVNPathUtil.canonicalizePath(rootURL.getProtocol() + ":" + myURL));
            return getResolvedURL();
        } else if (myURL.startsWith("/")) {
            // /      relative to the server's host
            myResolvedURL = ownerURL.setPath(myURL, true);
            return getResolvedURL();
        }
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Unrecognized format for the relative external URL ''{0}''.", myURL);
        SVNErrorManager.error(err, SVNLogType.DEFAULT);
        return null;
    }

    public String toString() {
        String value = "";
        String path = quotePath(myPath);
        String url = quotePath(myURL);
        
        if (myIsPegRevisionExplicit && SVNRevision.isValidRevisionNumber(myPegRevision.getNumber())) {
            if (myIsRevisionExplicit && SVNRevision.isValidRevisionNumber(myRevision.getNumber())) {
                value += "-r" + myRevision + " ";
            }
            value += url + "@" + myPegRevision + " " + path;
        } else {
            if (myIsNewFormat) {
                if (myIsRevisionExplicit && SVNRevision.isValidRevisionNumber(myRevision.getNumber())) {
                    value += "-r" + myRevision + " ";
                }
                value += url + " " + path;
            } else {
                value += path; 
                if (myIsRevisionExplicit && SVNRevision.isValidRevisionNumber(myRevision.getNumber())) {
                    value += " -r" + myRevision;
                }            
                value += " " + url;
            }
        }
        return value;
    }

    public static SVNExternal[] parseExternals(Object owner, String description) throws SVNException {
        List lines = new ArrayList();
        for(StringTokenizer tokenizer = new StringTokenizer(description, "\r\n"); tokenizer.hasMoreTokens();) {
            lines.add(tokenizer.nextToken());
        }
        Collection externals = new ArrayList();
        for (int i = 0; i < lines.size(); i++) {
            String line = ((String) lines.get(i)).trim();
            if ("".equals(line) || line.startsWith("#")) {
                continue;
            }
            List tokens = new ArrayList();
            for(Iterator tokenizer = new ExternalTokenizer(line); tokenizer.hasNext();) {
                tokens.add(tokenizer.next());
            }
            if (tokens.size() < 2 || tokens.size() > 4) {
                reportParsingError(owner, line);
            }
            SVNExternal external = new SVNExternal();
            int revisionToken = fetchRevision(external, owner, line, tokens);
            String token0 = (String) tokens.get(0);
            String token1 = (String) tokens.get(1);
            boolean token0isURL = SVNPathUtil.isURL(token0); 
            boolean token1isURL = SVNPathUtil.isURL(token1);
            
            if (token0isURL && token1isURL) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION, 
                        "Invalid svn:external property on ''{0}'': cannot use two absolute URLs (''{1}'' and ''{2}'') in an external; " +
                        "one must be a path where an absolute or relative URL is checked out to", new Object[] {owner, token0, token1});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (revisionToken == 0 && token1isURL) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION, 
                        "Invalid svn:external property on ''{0}'': cannot use a URL ''{1}'' as the target directory for an external definition",
                        new Object[] {owner, token1});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (revisionToken == 1 && token0isURL) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION, 
                        "Invalid svn:external property on ''{0}'': cannot use a URL ''{1}'' as the target directory for an external definition",
                        new Object[] {owner, token0});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            
            if (revisionToken == 0 || (revisionToken == -1 && (token0isURL || !token1isURL))) {
                external.myPath = token1;
                boolean schemeRelative = token0.startsWith("//");
                if (schemeRelative) {
                    token0 = token0.substring(2);
                }
            
                SVNPath path = new SVNPath(token0, true);
                external.myURL = schemeRelative ? "//" + path.getTarget() : path.getTarget();
                external.myRawURL = ((schemeRelative ? "//" : "") + (token0.contains("@") ? token0.substring(0, token0.lastIndexOf('@')) : token0));
                external.myPegRevision = path.getPegRevision();
                if (external.myPegRevision == SVNRevision.BASE) {
                    external.myPegRevision = SVNRevision.HEAD;
                }
                
                if (external.myPegRevision != SVNRevision.UNDEFINED) {
                    external.myIsPegRevisionExplicit = true;
                }
                
                external.myIsNewFormat = true;
            } else {
                external.myPath = token0;
                external.myURL = token1;
                external.myRawURL = token1;
                external.myPegRevision = external.myRevision;
            }
            
            if (external.myPegRevision == SVNRevision.UNDEFINED) {
                external.myPegRevision = SVNRevision.HEAD;
            } 
            
            if (external.myRevision == SVNRevision.UNDEFINED) {
                external.myRevision = external.myPegRevision;
            }
            
            external.myPath = SVNPathUtil.canonicalizePath(external.myPath.replace(File.separatorChar, '/'));
            
            if (external.myPath.length() == 0 || 
                    external.myPath.equals(".") || 
                    external.myPath.equals("..") || 
                    external.myPath.startsWith("../") || 
                    external.myPath.startsWith("/") || 
                    external.myPath.indexOf("/../") > 0 || 
                    external.myPath.endsWith("/..")) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
                        "Invalid {0} property on ''{1}'': target ''{2}'' is an absolute path or involves ''..''", new Object[] {SVNProperty.EXTERNALS, owner, external.myPath});
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }
            
            external.myRawValue = line;
            if (external.myURL != null && SVNPathUtil.isURL(external.myURL)) {
                SVNURL.parseURIEncoded(external.myURL);
            }
            externals.add(external);
        }
        return (SVNExternal[]) externals.toArray(new SVNExternal[externals.size()]);
    }
    
    private static String quotePath(String path) {
        for(int i = 0; i < path.length(); i++) {
            if (Character.isWhitespace(path.charAt(i))) {
                return "\"" + path + "\"";
            }
        }
        return path;
    }
    
    private static int fetchRevision(SVNExternal external, Object owner, String line, List tokens) throws SVNException {
        for (int i = 0; i < tokens.size() && i < 2; i++) {
            String token = (String) tokens.get(i);
            String revisionStr = null;
            if (token.length() >= 2 && 
                    token.charAt(0) == '-' && token.charAt(1) == 'r') {
                if (token.length() == 2 && tokens.size() == 4) {
                    revisionStr = (String) tokens.get(i + 1);
                    // remove separate '-r' token.
                    tokens.remove(i);
                } else if (tokens.size() == 3) {
                    revisionStr = token.substring(2); 
                }
                if (revisionStr == null || "".equals(revisionStr)) {
                    reportParsingError(owner, line);
                }
                long revNumber = -1;
                try {
                    revNumber = Long.parseLong(revisionStr);
                    if (revNumber < 0) {
                        SVNDebugLog.getDefaultLog().logFine(SVNLogType.DEFAULT, "Negative revision number found parsing '" + revisionStr + "'");
                        reportParsingError(owner, line);
                    }
                } catch (NumberFormatException nfe) {
                    SVNDebugLog.getDefaultLog().logFine(SVNLogType.DEFAULT, "Invalid revision number found parsing '" + revisionStr + "'");
                    reportParsingError(owner, line);
                }
                external.myRevision = SVNRevision.create(revNumber);
                external.myIsRevisionExplicit = true;
                tokens.remove(i);
                return i;
            }
        }
        if (tokens.size() == 2) {
            return -1;
        }
        reportParsingError(owner, line);
        return -1;
    }
    
    private static void reportParsingError(Object owner, String line) throws SVNException {
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
                "Error parsing {0} property on ''{1}'': ''{2}''", new Object[] {SVNProperty.EXTERNALS, owner, line});
        SVNErrorManager.error(err, SVNLogType.DEFAULT);
    }

    public static List<String> findTargetDuplications(SVNExternal[] externals) {
        List<String> paths = new ArrayList<String>();
        for (int i = 0; i < externals.length; i++) {
            final SVNExternal external = externals[i];
            String path = external.getPath();

            for (int j = i + 1; j < externals.length; j++) {
                SVNExternal anotherExternal = externals[j];
                String anotherPath = anotherExternal.getPath();

                if (path.equals(anotherPath)) {
                    paths.add(path);
                }
            }
        }
        return paths;
    }

    public String getRevisionString() {
        return isRevisionExplicit() ? formatRevisionString("-r", getRevision()) : null;
    }

    public String getPegRevisionString() {
        return isPegRevisionExplicit() ? formatRevisionString("@", getPegRevision()) : null;
    }

    private String formatRevisionString(String revisionPrefix, SVNRevision revision) {
        if (revision == null) {
            return null;
        } else if (revision == SVNRevision.HEAD) {
            return revisionPrefix + "HEAD";
        } else if (revision.getDate() != null) {
            return revisionPrefix + "{" + SVNDate.formatDate(revision.getDate(), true) + "}";
        } else if (SVNRevision.isValidRevisionNumber(revision.getNumber())) {
            return revisionPrefix + revision.getNumber();
        } else {
            return null;
        }
    }

    private static class ExternalTokenizer implements Iterator {
        
        private String myNextToken;
        private String myLine;

        public ExternalTokenizer(String line) {
            myLine = line;
            myNextToken = advance();
        }
        public boolean hasNext() {
            return myNextToken != null;
        }
        public Object next() {
            String next = myNextToken;
            myNextToken = advance();
            return next;
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
        
        private String advance() {
            while(myLine.length() > 0 && Character.isWhitespace(myLine.charAt(0))) {
                myLine = myLine.substring(1);
            }
            if (myLine.length() == 0) {
                return null;
            }
            char ch = myLine.charAt(0);
            int quouteType = ch == '\'' ? 1 : (ch == '\"' ? 2 : 0);
            if (quouteType != 0) {
                myLine = myLine.substring(1);
            }
            int index = 0;
            StringBuffer result = new StringBuffer();
            while(index < myLine.length()) {
                ch = myLine.charAt(index);                
                if (quouteType == 0) {
                    if (Character.isWhitespace(ch)) {
                        break;
                    }
                } else if (quouteType == 1) {
                    if (ch == '\'') {
                        break;
                    }
                } else if (quouteType == 2) {
                    if (ch == '\"') {
                        break;
                    }
                }
                if (ch == '\\') {
                    // append qouted character, so far whitespace only
                    if (index + 1 < myLine.length()) {
                        char escaped = myLine.charAt(index + 1);
                        if (escaped == ' ' || escaped == '\'' || escaped == '\"') {
                            // append escaped char instead of backslash
                            result.append(escaped);
                            index++;
                            index++;
                            continue;
                        }
                    } 
                }
                result.append(ch);
                index++;
                
            }
            if (index + 1 < myLine.length()) {
                myLine = myLine.substring(index + 1);
            } else {
                myLine = "";
            }
            return result.toString();
        }
    }

}
