package org.basex.http.webdav;

import static com.bradmcevoy.http.LockResult.*;
import static org.basex.http.webdav.WebDAVUtils.*;

import java.io.*;
import java.util.*;

import com.bradmcevoy.http.LockInfo.LockDepth;
import com.bradmcevoy.http.LockInfo.LockScope;
import com.bradmcevoy.http.LockInfo.LockType;
import com.bradmcevoy.http.Request.Method;

import org.basex.core.StaticOptions.AuthMethod;
import org.basex.http.*;
import org.basex.util.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

import com.bradmcevoy.http.*;
import com.bradmcevoy.http.exceptions.*;

/**
 * WebDAV resource representing an abstract folder within a collection database.
 *
 * @author BaseX Team 2005-16, BSD License
 * @author Rositsa Shadura
 * @author Dimitar Popov
 */
abstract class WebDAVResource implements CopyableResource, DeletableResource, MoveableResource,
    LockableResource {

  /** Resource meta data. */
  final WebDAVMetaData meta;
  /** WebDAV service implementation. */
  final WebDAVService service;

  /**
   * Constructor.
   * @param meta resource meta data
   * @param service service
   */
  WebDAVResource(final WebDAVMetaData meta, final WebDAVService service) {
    this.meta = meta;
    this.service = service;
  }

  @Override
  public Object authenticate(final String user, final String pass) {
    if(user == null) return null;
    return new WebDAVCode<Object>(this) {
      @Override
      public String get() throws IOException {
        service.authenticate(user, pass);
        return user;
      }
    }.evalNoEx();
  }

  @Override
  public boolean authorise(final Request request, final Method method, final Auth auth) {
    // use WebDAV authorization if no default user exists or if digest authentication is specified
    final HTTPContext http = service.http;
    if(http.username.isEmpty() || http.auth == AuthMethod.DIGEST) {
      if(auth == null || auth.getTag() == null) return false;
    }
    return WebDAVService.authorize(meta.db);
  }

  @Override
  public String checkRedirect(final Request request) {
    return null;
  }

  @Override
  public String getRealm() {
    return Prop.NAME;
  }

  @Override
  public String getUniqueId() {
    return null;
  }

  @Override
  public String getName() {
    return name(meta.path);
  }

  @Override
  public Date getModifiedDate() {
    return meta.mdate;
  }

  @Override
  public void delete() throws BadRequestException, NotAuthorizedException {
    new WebDAVCode<Object>(this) {
      @Override
      public void run() throws IOException {
        del();
      }
    }.eval();
  }

  @Override
  public void copyTo(final CollectionResource target, final String name) throws BadRequestException,
      NotAuthorizedException {

    new WebDAVCode<Object>(this) {
      @Override
      public void run() throws IOException {
        if(target instanceof WebDAVRoot)
          copyToRoot(name);
        else if(target instanceof WebDAVFolder)
          copyTo((WebDAVFolder) target, name);
      }
    }.eval();
  }

  @Override
  public void moveTo(final CollectionResource target, final String name) throws BadRequestException,
      NotAuthorizedException {

    new WebDAVCode<Object>(this) {
      @Override
      public void run() throws IOException {
        if(target instanceof WebDAVRoot)
          moveToRoot(name);
        else if(target instanceof WebDAVFolder)
          moveTo((WebDAVFolder) target, name);
      }
    }.eval();
  }

  /**
   * Lock this resource and return a token.
   *
   * @param timeout - in seconds, or null
   * @param lockInfo lock info
   * @return - a result containing the token representing the lock if successful,
   * otherwise a failure reason code
   */
  @Override
  public LockResult lock(final LockTimeout timeout, final LockInfo lockInfo)
      throws NotAuthorizedException, PreConditionFailedException, LockedException {

    return new WebDAVCode<LockResult>(this) {
      @Override
      public LockResult get() {
        return lockResource(timeout, lockInfo);
      }
    }.evalNoEx();
  }

  /**
   * Renew the lock and return new lock info.
   *
   * @param token lock token
   * @return lock result
   */
  @Override
  public LockResult refreshLock(final String token) throws NotAuthorizedException,
      PreConditionFailedException {
    return new WebDAVCode<LockResult>(this) {
      @Override
      public LockResult get() throws IOException {
        return refresh(token);
      }
    }.evalNoEx();
  }

  /**
   * If the resource is currently locked, and the tokenId  matches the current
   * one, unlock the resource.
   *
   * @param tokenId lock token
   */
  @Override
  public void unlock(final String tokenId) throws NotAuthorizedException,
      PreConditionFailedException {
    new WebDAVCode<Object>(this) {
      @Override
      public void run() throws IOException {
        service.locking.unlock(tokenId);
      }
    }.evalNoEx();
  }

  /**
   * Get the active lock for the current resource.
   * @return - the current lock if the resource is locked, or null
   */
  @Override
  public LockToken getCurrentLock() {
    return new WebDAVCode<LockToken>(this) {
      @Override
      public LockToken get() throws IOException {
        return getCurrentActiveLock();
      }
    }.evalNoEx();
  }

  /**
   * Delete document or folder.
   * @throws IOException I/O exception
   */
  void del() throws IOException {
    service.delete(meta.db, meta.path);
  }

  /**
   * Rename document or folder.
   * @param n new name
   * @throws IOException I/O exception
   */
  void rename(final String n) throws IOException {
    service.rename(meta.db, meta.path, n);
  }

  /**
   * Copy folder to the root, creating a new database.
   * @param n new name of the folder (database)
   * @throws IOException I/O exception
   */
  protected abstract void copyToRoot(final String n) throws IOException;

  /**
   * Copy folder to another folder.
   * @param f target folder
   * @param n new name of the folder
   * @throws IOException I/O exception
   */
  protected abstract void copyTo(final WebDAVFolder f, final String n) throws IOException;

  /**
   * Move folder to the root, creating a new database.
   * @param n new name of the folder (database)
   * @throws IOException I/O exception
   */
  void moveToRoot(final String n) throws IOException {
    // folder is moved to the root: create new database with it
    copyToRoot(n);
    del();
  }

  /**
   * Move folder to another folder.
   * @param f target folder
   * @param n new name of the folder
   * @throws IOException I/O exception
   */
  private void moveTo(final WebDAVFolder f, final String n) throws IOException {
    if(f.meta.db.equals(meta.db)) {
      // folder is moved to a folder in the same database
      rename(f.meta.path + SEP + n);
    } else {
      // folder is moved to a folder in another database
      copyTo(f, n);
      del();
    }
  }

  /**
   * Lock the current resource.
   * @param timeout lock timeout
   * @param lockInfo lock info
   * @return lock result
   */
  private LockResult lockResource(final LockTimeout timeout, final LockInfo lockInfo) {
    try {
      final String tokenId = service.locking.lock(
        meta.db,
        meta.path,
        lockInfo.scope.name().toLowerCase(Locale.ENGLISH),
        lockInfo.type.name().toLowerCase(Locale.ENGLISH),
        lockInfo.depth.name().toLowerCase(Locale.ENGLISH),
        lockInfo.lockedByUser,
        timeout.getSeconds()
      );
      return success(new LockToken(tokenId, lockInfo, timeout));
    } catch(final IOException ex) {
      return failed(FailureReason.ALREADY_LOCKED);
    }
  }

  /**
   * Get the active lock on the current resource.
   * @return the token of the active lock or {@code null} if resource is not locked
   * @throws IOException I/O exception
   */
  private LockToken getCurrentActiveLock() throws IOException {
    final String lockInfoStr = service.locking.lock(meta.db, meta.path);
    return lockInfoStr == null ? null : parseLockInfo(lockInfoStr);
  }

  /**
   * Renew a lock with the given token.
   * @param token lock token
   * @return lock result
   * @throws IOException I/O exception
   */
  private LockResult refresh(final String token) throws IOException {
    service.locking.refreshLock(token);
    final String lockInfoStr = service.locking.lock(token);
    final LockToken lockToken = lockInfoStr == null ? null : parseLockInfo(lockInfoStr);
    return lockToken == null ? failed(FailureReason.ALREADY_LOCKED) : success(lockToken);
  }

  /**
   * Parse the lock info.
   * @param lockInfo lock info as a string
   * @return parsed lock info bean
   * @throws IOException I/O exception
   */
  private static LockToken parseLockInfo(final String lockInfo) throws IOException {
    try {
      final XMLReader reader = XMLReaderFactory.createXMLReader();
      final LockTokenSaxHandler handler = new LockTokenSaxHandler();
      reader.setContentHandler(handler);
      reader.parse(new InputSource(new StringReader(lockInfo)));
      return handler.lockToken;
    } catch(final SAXException ex) {
      Util.errln("Error while parsing lock info: %", ex);
      return null;
    }
  }

  /** SAX handler for lock token. */
  private static final class LockTokenSaxHandler extends DefaultHandler {
    /** Parsed lock token. */
    private final LockToken lockToken = new LockToken(null, new LockInfo(), null);
    /** Current element name. */
    private String elementName;

    @Override
    public void startElement(final String uri, final String localName, final String name,
        final Attributes attributes) throws SAXException {
      elementName = localName;
      super.startElement(uri, localName, name, attributes);
    }

    @Override
    public void endElement(final String uri, final String localName, final String qName)
        throws SAXException {
      elementName = null;
      super.endElement(uri, localName, qName);
    }

    @Override
    public void characters(final char[] ch, final int start, final int length) {
      final String v = String.valueOf(ch, start, length);
      if("token".equals(elementName))
        lockToken.tokenId = v;
      else if("scope".equals(elementName))
        lockToken.info.scope = LockScope.valueOf(v.toUpperCase(Locale.ENGLISH));
      else if("type".equals(elementName))
        lockToken.info.type = LockType.valueOf(v.toUpperCase(Locale.ENGLISH));
      else if("depth".equals(elementName))
        lockToken.info.depth = LockDepth.valueOf(v.toUpperCase(Locale.ENGLISH));
      else if("owner".equals(elementName))
        lockToken.info.lockedByUser = v;
      else if("timeout".equals(elementName))
        lockToken.timeout = LockTimeout.parseTimeout(v);
    }
  }
}
