package org.apache.slide.projector.repository.webdav;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Logger;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpConnection;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.i18n.LocalizedError;
import org.apache.slide.projector.ContentType;
import org.apache.slide.projector.Projector;
import org.apache.slide.projector.repository.AccessDeniedException;
import org.apache.slide.projector.repository.Configurable;
import org.apache.slide.projector.repository.NotFoundException;
import org.apache.slide.projector.repository.Repository;
import org.apache.slide.projector.repository.RepositoryException;
import org.apache.slide.projector.repository.RoleExistsException;
import org.apache.slide.projector.repository.UserExistsException;
import org.apache.slide.projector.value.ElementValue;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.Text;
import org.apache.slide.projector.value.TypedInputStream;
import org.apache.slide.projector.value.URI;
import org.apache.slide.projector.value.URIValue;
import org.apache.slide.projector.value.XMLValue;
import org.apache.webdav.lib.NotificationListener;
import org.apache.webdav.lib.Property;
import org.apache.webdav.lib.PropertyName;
import org.apache.webdav.lib.Subscriber;
import org.apache.webdav.lib.methods.MkcolMethod;
import org.apache.webdav.lib.methods.PropFindMethod;
import org.apache.webdav.lib.methods.PropPatchMethod;
import org.apache.webdav.lib.methods.SearchMethod;
import org.jdom.Element;
import org.jdom.input.DOMBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

public class WebdavRepository implements Repository, Configurable {
    private static Logger logger = Logger.getLogger(WebdavRepository.class.getName());

    private static final String SLIDE_NAMESPACE = "http://jakarta.apache.org/slide/";
    private static final String DAV_NAMESPACE = "DAV:";

    private Protocol protocol;
    private String host, domain, notificationHost, users, roles;  
    private int port, notificationPort, notificationDelay, pollInterval, subscriptionLifetime;  
    
    private static WebdavRepository repository = new WebdavRepository();
    private NotificationListener dispatcher;

    private WebdavRepository() {}

    public static Repository getInstance() {
        return repository;
    }

	public void configure(Element configuration) {
		protocol = Protocol.getProtocol(configuration.getChildText("protocol"));
		host = configuration.getChildText("host");
		port = Integer.valueOf(configuration.getChildText("port")).intValue();
		domain = configuration.getChildText("domain");
		users = configuration.getChildText("users");
		roles = configuration.getChildText("roles");
		Element notifications = configuration.getChild("notifications");
		notificationHost = notifications.getChildText("host");
		notificationPort = Integer.valueOf(notifications.getChildText("port")).intValue();
		pollInterval = Integer.valueOf(notifications.getChildText("poll-interval")).intValue();
		subscriptionLifetime = Integer.valueOf(notifications.getChildText("subscription-lifetime")).intValue();
		notificationDelay = Integer.valueOf(notifications.getChildText("notification-delay")).intValue();
  		dispatcher = new NotificationListener(notificationHost, notificationPort,
    				host, port, protocol, Projector.getCredentials(), domain, pollInterval, true);
	}

	public Credentials login(String username, String password) throws RepositoryException {
    	String url = domain+Projector.getProjectorDir(); 
    	GetMethod getMethod = new GetMethod(url);
        getMethod.setDoAuthentication(true);
        Credentials credentials = new UsernamePasswordCredentials(username, password);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        int state;
        try {
            state = getMethod.execute(httpState, new HttpConnection(host, port, protocol));
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
        if ( state == HttpStatus.SC_OK ) {
            return credentials;
        } 
        throw new RepositoryException(new LocalizedError("httpError", new Object[] { new Integer(state) }));
    }
    
    public URI createUser(String username, String password, Credentials credentials) throws RepositoryException {
    	try {
    	    getResource(new URIValue(users+username), credentials);
        } catch ( NotFoundException exception ) {
            URI userUri = new URIValue(users+username);
            MkcolMethod mkcolMethod = new MkcolMethod(domain+userUri);
            mkcolMethod.setDoAuthentication(true);
            HttpState httpState = new HttpState();
            httpState.setCredentials(null, host, credentials);
            int state;
            try {
                state = mkcolMethod.execute(httpState, new HttpConnection(host, port, protocol));
            } catch (IOException e) {
                throw new RepositoryException(new LocalizedError("repositoryException"), e);
            }
            if ( state == HttpStatus.SC_CREATED ) {
                changePassword(userUri, null, password, credentials);
                return userUri;
            } else if ( state == HttpStatus.SC_FORBIDDEN ) {
                throw new AccessDeniedException(new LocalizedError("userCreationForbidden", new String[] { username }));
            }
            throw new WebdavException(new LocalizedError("httpError", new Object[] { new Integer(state) }), state);
        }
        throw new UserExistsException(new LocalizedError("userAlreadyExists", new String[] { username }));
    }
    
    public URI createRole(String rolename, Credentials credentials) throws RepositoryException {
        try {
            getResource(new URIValue(roles+rolename), credentials);
        } catch ( NotFoundException exception ) {
            URI roleUri = new URIValue(roles+rolename);
            MkcolMethod mkcolMethod = new MkcolMethod(domain+roleUri);
            mkcolMethod.setDoAuthentication(true);
            HttpState httpState = new HttpState();
            httpState.setCredentials(null, host, credentials);
            int state;
            try {
                state = mkcolMethod.execute(httpState, new HttpConnection(host, port, protocol));
            } catch (IOException e) {
                throw new RepositoryException(new LocalizedError("repositoryException"), e);
            }
            if ( state == HttpStatus.SC_CREATED ) {
                return roleUri;
            } else if ( state == HttpStatus.SC_FORBIDDEN ) {
                throw new AccessDeniedException(new LocalizedError("roleCreationForbidden", new String[] { rolename }));
            }
            throw new WebdavException(new LocalizedError("httpError", new Object[] { new Integer(state) }), state);
        }
        throw new RoleExistsException(new LocalizedError("userExists", new String[] { rolename }));
    }
    
    public void deleteRole(URI role, Credentials credentials) throws RepositoryException {
    	removeResource(role, credentials);
    }
    
    public void deleteUser(URI user, Credentials credentials) throws RepositoryException {
    	removeResource(user, credentials);
    }
    
    public void addRole(URI user, URI role, Credentials credentials) throws RepositoryException {
    	XMLValue membersElement = getPropertyAsXMLValue(role, DAV_NAMESPACE, "group-member-set", credentials);
    	// remove member from role to avoid double membership
        removeRole(membersElement.getRootElement().getChildren(), domain+user);
        Element newUserElement = new Element("href", "DAV:");
        newUserElement.addContent(user.toString());
        membersElement.getRootElement().addContent(newUserElement);
        XMLOutputter xout = new XMLOutputter(Format.getCompactFormat());
        String groupMemberSet = xout.outputString(membersElement.getRootElement().getContent());
		setProperty(role, DAV_NAMESPACE, "group-member-set", groupMemberSet, credentials);    
    }
    
    public void removeRole(URI user, URI role, Credentials credentials) throws RepositoryException {
    	XMLValue membersElement = getPropertyAsXMLValue(role, DAV_NAMESPACE, "group-member-set", credentials);
        removeRole(membersElement.getRootElement().getChildren(), domain+user);
        XMLOutputter xout = new XMLOutputter(Format.getCompactFormat());
        String groupMemberSet = xout.outputString(membersElement.getRootElement().getContent());
		setProperty(role, DAV_NAMESPACE, "group-member-set", groupMemberSet, credentials);    
    }
    
    private void removeRole(List members, String user) {
        for ( Iterator i = members.iterator(); i.hasNext(); ) {
            Element memberElement = (Element)i.next();
            String uri = memberElement.getTextTrim();
            if ( uri.equals(user) ) {
                i.remove();
                break;
            }
        }
    }
    
    public List listRoles(URI user, Credentials credentials) throws RepositoryException {
    	XMLValue membership = getPropertyAsXMLValue(user, DAV_NAMESPACE, "group-membership", credentials);
    	List roles = membership.getRootElement().getChildren();
    	List roleList = new ArrayList();
    	for ( Iterator i = roles.iterator(); i.hasNext(); ) {
    		Map map = new HashMap();
    		Element roleElement = (Element)i.next();
    		String uri = roleElement.getTextTrim();
			if ( uri.indexOf(domain) != -1  ) {
				uri = uri.substring(uri.indexOf(domain)+domain.length());
			}
    		map.put("uri", uri);
    		roleList.add(map);
    	}
    	return roleList;
    }
    
    public void changePassword(URI uri, String oldPassword, String newPassword, Credentials credentials) throws RepositoryException {
		setProperty(uri, SLIDE_NAMESPACE, "password", newPassword, credentials);
    }

    public TypedInputStream getResource(URI uri, Credentials credentials) throws RepositoryException {
        String url = domain+uri;
        GetMethod getMethod = new GetMethod(url);
        getMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            int state = getMethod.execute(httpState, new HttpConnection(host, port, protocol));
            if ( state == HttpStatus.SC_NOT_FOUND ) {
                throw new NotFoundException(new LocalizedError("resourceNotFound", new Object[] { uri }));
            } else if ( state == HttpStatus.SC_FORBIDDEN ) {
                throw new AccessDeniedException(new LocalizedError("readAccessDenied", new Object[] { uri }));
            } else if ( state != HttpStatus.SC_OK ) {
                throw new WebdavException(new LocalizedError("httpError", new Object[] { new Integer(state) }), state);
            }
            InputStream stream = getMethod.getResponseBodyAsStream();
            String contentType = getMethod.getResponseHeader("Content-type").getValue();
            String characterEncoding = getMethod.getResponseCharSet();
            Header contentLengthHeader = getMethod.getResponseHeader("Content-length");
            if ( contentLengthHeader == null ) return null;
            int contentLength = Integer.parseInt(getMethod.getResponseHeader("Content-length").getValue());
            return new TypedInputStream(stream, contentType, characterEncoding, contentLength, true);
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
    }

    public void setProperty(URI uri, String namespace, String name, String value, Credentials credentials) throws RepositoryException {
        PropPatchMethod proppatchMethod = new PropPatchMethod(domain+uri);
        proppatchMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        proppatchMethod.addPropertyToSet(name, value, "S", namespace);
        try {
            int state = proppatchMethod.execute(httpState, new HttpConnection(host, port, protocol));
            if ( state != HttpStatus.SC_MULTI_STATUS ) {
                throw new RepositoryException(new LocalizedError("httpError", new Object[] { new Integer(state) }));
            }
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
    }

    public String getPropertyAsStringValue(URI uri, String namespace, String name, Credentials credentials) throws RepositoryException {
		Property property = getProperty(uri, namespace, name, credentials);
		if ( property == null ) return null;
        return property.getPropertyAsString();
    }

    public XMLValue getPropertyAsXMLValue(URI uri, String namespace, String name, Credentials credentials) throws RepositoryException {
		Property property = getProperty(uri, namespace, name, credentials);
		if ( property == null ) return null;
		DOMBuilder builder = new DOMBuilder();
        Element element = builder.build(property.getElement());
		return new ElementValue(element);
    }

	private Property getProperty(URI uri, String namespace, String name, Credentials credentials) throws RepositoryException {
		String url = domain+uri;
        Vector props = new Vector();
        props.add(new PropertyName(namespace, name));
        PropFindMethod propfindMethod = new PropFindMethod(url, 0, props.elements());
        propfindMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            int state = propfindMethod.execute(httpState, new HttpConnection(host, port, protocol));
            if ( state != HttpStatus.SC_MULTI_STATUS ) {
                throw new RepositoryException(new LocalizedError("httpError", new Object[] { new Integer(state) }));
            }
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
        Enumeration propertyEnumeration = propfindMethod.getResponseProperties(url);
        if ( propertyEnumeration.hasMoreElements() ) {
        	return (Property)propertyEnumeration.nextElement();
        }
        return null;
    }

    public List getProperties(URI uri, Credentials credentials) throws RepositoryException {
        String url = domain+uri;
        PropFindMethod propfindMethod = new PropFindMethod(url, 0);
        propfindMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            int state = propfindMethod.execute(httpState, new HttpConnection(host, port, protocol));
            if ( state != HttpStatus.SC_MULTI_STATUS ) {
                throw new RepositoryException(new LocalizedError("httpError", new Object[] { new Integer(state) }));
            }
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
        List arrayList = new ArrayList();
        for ( Enumeration propertyEnumeration = propfindMethod.getResponseProperties(url); propertyEnumeration.hasMoreElements(); ) {
            Map properties = new HashMap();
            Property property = (Property)propertyEnumeration.nextElement();
            properties.put("name", new Text(property.getLocalName()));
            properties.put("value", new Text(property.getPropertyAsString()));
            arrayList.add(properties);
        }
        return arrayList;
    }

    public void setResource(URI uri, Streamable resource, Credentials credentials) throws RepositoryException {
        String url = domain+uri;
        PutMethod putMethod = new PutMethod(url);
        putMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            putMethod.setRequestBody(resource.getInputStream());
            putMethod.setRequestHeader("Content-type", ContentType.getContentType(resource));
            putMethod.setRequestHeader("Content-length", String.valueOf(resource.getContentLength()));
            putMethod.execute(httpState, new HttpConnection(host, port, protocol));
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
    }

    public void removeResource(URI uri, Credentials credentials) throws RepositoryException {
        String url = domain+uri;
        DeleteMethod deleteMethod = new DeleteMethod(url);
        deleteMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            deleteMethod.execute(httpState, new HttpConnection(host, port, protocol));
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
    }

    public List getChildren(URI uri, Credentials credentials) throws RepositoryException {
        String url = domain+uri;
        if ( url.charAt(url.length()-1) == '/') {
        	url = url.substring(0, url.length()-1);
        }
        PropFindMethod propfindMethod = new PropFindMethod(url, 0);
        propfindMethod.setDepth(1);
        propfindMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            int state = propfindMethod.execute(httpState, new HttpConnection(host, port, protocol));
            if ( state != HttpStatus.SC_MULTI_STATUS ) {
                throw new RepositoryException(new LocalizedError("httpError", new Object[] { new Integer(state) }));
            }
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
        List children = new ArrayList();
            for ( Enumeration propertyEnumeration = propfindMethod.getAllResponseURLs(); propertyEnumeration.hasMoreElements(); ) {
        	String childUrl = (String)propertyEnumeration.nextElement();
        	if ( !childUrl.equals(url) ) {
    			if ( childUrl.indexOf(domain) != -1  ) {
    				childUrl = childUrl.substring(childUrl.indexOf(domain)+domain.length());
    			}
        		children.add(new URIValue(childUrl));
        	}
        }
        return children;
    }
    
    public void subscribe(String method, URI uri, int depth, Subscriber listener, Credentials credentials) {
        dispatcher.subscribe(method, uri.toString(), depth, subscriptionLifetime, notificationDelay, listener, credentials);
    }
    
    public void subscribe(String method, URI uri, int depth, int lifetime, int notificationDelay, Subscriber listener, Credentials credentials) {
        dispatcher.subscribe(method, uri.toString(), depth, lifetime, notificationDelay, listener, credentials);
    }
    
    public void unsubscribe(URI uri, Subscriber listener, Credentials credentials) {
        dispatcher.unsubscribe(uri.toString(), listener, credentials);
    }
    
    public void fireEvent(Map information, Credentials credentials) throws RepositoryException {
        try {
            dispatcher.fireEvent(information, credentials);
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
    }

    public void fireVetoableEvent(Map information, Credentials credentials) throws RepositoryException {
        try {
            dispatcher.fireVetoableEvent(information, credentials);
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
    }

    public List search(String query, Credentials credentials) throws RepositoryException {
        String url = domain+"/";
        SearchMethod searchMethod = new SearchMethod(url, query);
        searchMethod.setDoAuthentication(true);
        HttpState httpState = new HttpState();
        httpState.setCredentials(null, host, credentials);
        try {
            int state = searchMethod.execute(httpState, new HttpConnection(host, port, protocol));
            if ( state != HttpStatus.SC_MULTI_STATUS ) {
                throw new RepositoryException(new LocalizedError("httpError", new Object[] { new Integer(state) }));
            }
        } catch (IOException e) {
            throw new RepositoryException(new LocalizedError("repositoryException"), e);
        }
        List values = new ArrayList();
        for ( Enumeration e = searchMethod.getAllResponseURLs(); e.hasMoreElements(); ) {
            Map searchResults = new HashMap(); 
            String uri = (String)e.nextElement();
            for ( Enumeration pe = searchMethod.getResponseProperties(uri); pe.hasMoreElements(); ) {
	            Property property = (Property)pe.nextElement();
				searchResults.put(property.getLocalName(), property.getPropertyAsString());
	        }
			if ( uri.indexOf(domain) != -1  ) {
				uri = uri.substring(uri.indexOf(domain)+domain.length());
			}
			searchResults.put("uri", uri);
			values.add(searchResults);
        }
        return values;
    }
}