// $Id: ModuleAddress.java,v 1.17 2004/06/14 20:56:12 m31 Exp $
//
// This file is part of the program suite "Principia Mathematica II"
// which is a working prototype for the main project:
// "Hilbert II" - http://www.qedeq.org
//
// Copyright 2001-2004  Michael Meyling <mime@qedeq.org>.
//
// "Principia Mathematica II" 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.
//
// This program 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. See the
// GNU General Public License for more details.

package com.meyling.principia.module;


import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

import com.meyling.principia.argument.ArgumentException;
import com.meyling.principia.argument.Text;
import com.meyling.principia.io.Utility;
import com.meyling.principia.log.Trace;


/**
 * An object of this class represents an address for a module.
 *
 * @version $Revision: 1.17 $
 * @author  Michael Meyling
 */
public class ModuleAddress {

    /** address */
    private final String address;

    /** URL form of this address */
    private final URL url;

    /** header (including protocol, host, port, user) but without file path */
    private final String header;

    /** path (without protocol, host, port and file name) */
    private final String path;

    /** name of module including version information and postfix */
    private final String fileName;

    /** is module address relative */
    private final boolean relativeAddress;

    /** name of module */
    private final Name name;

    /** moduleVersion */
    private final Version moduleVersion;

    /** ruleVersion */
    private final Version ruleVersion;


    /**
     * Constructor.
     *
     * @param   address  address of module
     * @param   parent  address of parent module
     * @throws  IOException if address is formaly incorrect
     */
    public ModuleAddress(String address, final ModuleAddress parent)
            throws IOException {

        final String METHOD = "Constructor(String, ModuleAddress)";
        if (address == null) {
            throw new IllegalArgumentException(
                ModuleConstants.NULLPOINTER_PARAMETER);
        }
        URL url;
        try {
            if (parent != null) {
                url = new URL(parent.url, address);
            } else {
                url = new URL(address);
            }
        } catch (MalformedURLException e) {
            Trace.trace(this, METHOD, "address=" + address);
            Trace.trace(this, METHOD, "parent=" + parent);
            Trace.trace(this, METHOD, e);
            try {
                address = "file:" + address;
                if (parent != null) {
                    url = new URL(parent.url, address);
                } else {
                    url = new URL(address);
                }
            } catch (MalformedURLException ex) {
                throw e;    // throw orginal exception
            }
        }
        Trace.trace(this, METHOD, "protocol=" + url.getProtocol());
        this.url = url;
        this.address = this.url.toExternalForm();
        Trace.trace(this, METHOD, "address=" + this.address);
/*
        Trace.trace(this, METHOD, "url.getFile=" + this.url.getFile());
        Trace.trace(this, METHOD, "url.getPath=" + this.url.getPath());
        try {
            Trace.trace(this, METHOD, "URI File=" + new File(new URI(this.address)).getAbsoluteFile());
        } catch (URISyntaxException e1) {
            e1.printStackTrace();
        }
*/
        final String p = url.getFile();
        final int position = p.lastIndexOf("/");
        if (position >= 0 && position + 1 < p.length()) {
            this.path = p.substring(0, position) + "/";
            this.fileName = p.substring(position + 1);
        } else {
            this.path = "";
            this.fileName = p;
        }
        Trace.trace(this, METHOD, "path=" + this.path);
        Trace.trace(this, METHOD, "fileName=" + this.fileName);
        this.relativeAddress = !this.path.startsWith("/");
        if (!this.fileName.endsWith(".qedeq")) {
            throw new IOException("file name doesn't end with \".qedeq\": "
                + this.fileName);
        }
        final int positionLast = this.fileName.lastIndexOf("_");
        if (positionLast >= 0
                && positionLast < this.fileName.length() - 6) {
            final String version = this.fileName.substring(
                positionLast + 1, this.fileName.length() - 6);
            try {
                this.ruleVersion = new Version(version);
            } catch (ArgumentException e) {
                throw new IOException(
                    "file name has incorrect rule version: "
                    + version + "\n" + e.getMessage());
            }
        } else {
            throw new IOException(
                "file name doesn't include rule version: "
                + this.fileName);
        }
        final int positionBefore = this.fileName.lastIndexOf("_",
            positionLast - 1);
        if (positionBefore > 0
                && positionBefore < positionLast) {
            final String version = this.fileName.substring(
                positionBefore + 1, positionLast);
            try {
                this.moduleVersion = new Version(version);
            } catch (ArgumentException e) {
                throw new IOException(
                    "file name has incorrect module version: "
                    + version + "\n" + e.getMessage());
            }
        } else {
            throw new IOException(
                "file name doesn't include module version: "
                + this.fileName);
        }
        final String mname = this.fileName.substring(0, positionBefore);
        try {
            this.name = new Name(mname);
        } catch (ArgumentException e) {
                throw new IOException(
                    "file name has incorrect module name: "
                    + mname + "\n" + e.getMessage());
        }
        final int positionPath
            = this.address.lastIndexOf(this.path + this.fileName);
        if (positionPath < 0) {
            throw new IllegalArgumentException(
                "couldn't determine begin of file path: "
                + this.address);
        }
        this.header = this.address.substring(0, positionPath);
    }


    /**
     * Constructor.
     *
     * @param   address  address of module
     * @throws  IOException if address is formaly incorrect
     */
    public ModuleAddress(final String address)
            throws IOException {
        this(address, (ModuleAddress) null);
    }


    /**
     * Constructor.
     *
     * @param   address  address of module
     * @throws  IOException if address is formaly incorrect
     */
    public ModuleAddress(final ModuleAddress oldAddress,
            final Name name, final Version moduleVersion,
            final Version ruleVersion)
            throws IOException {
        this(name.getText() + "_" + moduleVersion.getText() + "_"
            + ruleVersion.getText() + ".qedeq", oldAddress);
    }


    /**
     * Get module address.
     *
     * @return module address
     */
    public final String getAddress() {
        return this.address;
    }


    /**
     * Get address header (including protocol, host, port, user)
     * but without file path.
     *
     * @return address header
     */
    public final String getHeader() {
        return this.header;
    }


    /**
     * Get address path (without protocol, host, port and file name).
     *
     * @return module path
     */
    public final String getPath() {
        return this.path;
    }


    /**
     * Get name of module including version information and postfix.
     *
     * @return module name
     */
    public final String getFileName() {
        return this.fileName;
    }


    /**
     * Get name of module (without version information and postfix).
     *
     * @return module name
     */
    public final Name getName() {
        return this.name;
    }


    /**
     * Get fully qualified URL of module.
     *
     * @return module path
     */
    public final URL getURL() {
        return this.url;
    }


    /**
     * Get rule version of module.
     *
     * @return rule version
     */
    public final Version getRuleVersion() {
        return this.ruleVersion;
    }


    /**
     * Get version of module.
     *
     * @return module version
     */
    public final Version getModuleVersion() {
        return this.moduleVersion;
    }


    /**
     * Was this module address created relativly?
     *
     * @return relativly created?
     */
    public final boolean isRelativeAddress() {
        return this.relativeAddress;
    }


    public final boolean isFileAddress() {
        return getAddress().indexOf("file:") >= 0;
    }


    /**
     * Transform an URL address into a local file path where a
     * module could be buffered.
     *
     * @return  local file path for that <code>address</code>.
     */
    public final String getLocalName() {
        if (isFileAddress()) {
            return getPath() + getFileName();
        }
        return ModuleContext.getBufferDirectory() + localizeInFileSystem(getAddress());
    }


    /**
     * Transform an URL address into a local file path where a
     * generated module could saved.
     *
     * @return  local file path for that <code>address</code>.
     */
    public final String getGeneratedName() {
        return ModuleContext.getGenerationDirectory() + localizeInFileSystem(getAddress());
    }


    /**
     * Transform an URL address into a local file path where a
     * generated module or document could be saved.
     *
     * @param   ending  new ending for document
     * @return  local file path for that <code>address</code>.
     */
    public final String getGeneratedName(final String ending) {
        return newEnding(ModuleContext.getGenerationDirectory() + localizeInFileSystem(getAddress()), ending);
    }


    /**
     * Transform an URL address into a local file path.
     * 
     * @param   url transform this URL
     * @return  result of transformation
     */
    private final static String localizeInFileSystem(final String url) {
        StringBuffer adr = new StringBuffer(url);
        Utility.replace(adr, "://", "/");
        Utility.replace(adr, ":/", "/");
        Utility.replace(adr, ":", "$");
        Utility.replace(adr, "|", "$$");    // for win32
        return adr.toString();
    }


    /**
     * Create relative address from <code>orgin</code> to <code>next</code>.
     *
     * @param   reference    this should be the next location
     * @return  relative (or if neccessary absolute) address
     */
    public final String createRelativeAddress(
            final ModuleAddress reference) {
        return createRelativeAddress(this.getAddress(),
            reference.getAddress());
    }


    /** @inherited */
    public final String toString() {
        return this.address;
    }


    /** @inherited */
    public final int hashCode() {
        return this.address.hashCode();
    }


    /** @inherited */
    public final boolean equals(final Object object) {
        if (object == null || !(object instanceof ModuleAddress)) {
            return false;
        }
        return this.address.equals(((ModuleAddress) object).address);
    }


    /**
     * Get the file name of the specified module.
     *
     * @param   spec    here are the (perhaps relative) addresses to
     *                  another module
     * @return  file name of specified module
     */
    public final static String getModuleFileName(final Specification spec) {

        return
            // name
            ((Text) spec.getArgument(0).getArgument(0)).getText()
            + "_"
            // module version
            + ((Text) spec.getArgument(1).getArgument(0)).getText()
            + "_"
            // rule version
            + ((Text) spec.getArgument(2).getArgument(0)).getText()
            + ".qedeq";
    }


    /**
     * Get all potential module addresses from a module specifiaction.
     *
     * @param   module  starting from that module (has an absolute
     *                  address)
     * @param   spec    here are the (perhaps relative) addresses to
     *                  another module
     * @return  array of absolute address strings
     * @throws  ArgumentException    if an URL is malformed
     */
    final static ModuleAddress[] getModulePaths(final Module module,
            final Specification spec)
            throws ArgumentException {

        final String fileNameEnd = getModuleFileName(spec);
        final LocationList locations = (LocationList) spec.getArgument(3);
        final ModuleAddress[] result
            = new ModuleAddress[locations.getArgumentSize()];
        for (int i = 0; i < locations.getArgumentSize(); i++) {
            String fileName
                = ((Text) locations.getArgument(i).getArgument(0))
                .getText();
            if (fileName.equals(".")) {
                fileName = "";
            } else if (!fileName.endsWith("/")) {
                fileName += "/";
            }
            fileName += fileNameEnd;
            try {
                result[i] = new ModuleAddress(fileName,
                module.getModuleAddress());
            } catch (IOException e) {
                throw new ArgumentException(
                    ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                    e.getMessage(),
                    locations.getArgument(i).getArgument(0));
            }
        }
        return result;
    }


    /**
     * Transform an URL address into a local file path where a
     * module could be buffered
     *
     * @param   address     an URL
     * @return  local file path for that <code>address</code>
     * @throws  IOException    if <code>address</code>
     *          contains no &quot;://&quot;
     */
    public final static String getLocalName(String address) throws
            IOException {
        final ModuleAddress moduleAddress = new ModuleAddress(address);
        return moduleAddress.getLocalName();
    }


    /**
     * Get the file name of an module without path and protokoll.
     *
     * @param   address     an URL
     * @return  file name of that <code>address</code>
     * @throws  IOException if that was no valid module address
     */
    public final static String getModuleFileName(String address)
            throws IOException {
        final ModuleAddress moduleAddress = new ModuleAddress(address);
        return moduleAddress.getFileName();
    }


    /**
     * Create relative address from <code>orgin</code> to <code>next</code>.
     *
     * @param   orgin   this is the original location (URL!)
     * @param   next    this should be the next location (URL!)
     * @return  relative (or if neccessary absolute) address
     */
    public final static String createRelativeAddress(final String orgin,
            final String next) {
        if (orgin.equals(next)) {
            return "";
        }
        try {
            final URL urlOrgin = new URL(orgin);
            final URL urlNext = new URL(next);

            if (urlOrgin.getProtocol().equals(urlNext.getProtocol())
                    && urlOrgin.getHost().equals(urlNext.getHost())
                    && urlOrgin.getPort() == urlNext.getPort()) {
                final String org = urlOrgin.getFile();
                final String nex = urlNext.getFile();
                int i = -1; // position of next '/'
                int j = 0;  // position of last '/'
                while (0 <= (i = org.indexOf("/", j))) {
                    if (i >= 0 && nex.length() > i
                            && org.substring(j, i).equals(
                            nex.substring(j, i))) {
                        j = i + 1;
                    } else {
                        break;
                    }
                }
                if (j > 0) {
                    i = j;
                    StringBuffer result = new StringBuffer(nex.length());
                    while (0 <= (i = org.indexOf("/", i))) {
                        i++;
                        result.append("../");
                    }
                    result.append(nex.substring(j));
                    return result.toString();
                } else {
                    return "/" + nex;
                }
            } else {    // no relative address posible
                return urlNext.toString();
            }
        } catch (MalformedURLException e) {
            return next;
        }

    }


    /**
     * Get module address with new ending. E.g.: ".html" instead of ".qedeq".
     *
     * @param   address     the address of somthing (e.g.: a module)
     * @param   newEnding   this should be the new ending (e.g.: "html")
     * @return  module address with substituted ending
     */
    public final static String newEnding(final String address,
            final String newEnding) {
        if (address.length() == 0) {
            return "";
        }
        final int i = address.lastIndexOf(".");
        if (i > 0) {
            return address.substring(0, i + 1) + newEnding;
        } else {
            return address + "." + newEnding;
        }
    }

}
