/*
 * Copyright (c) 2014 AsyncHttpClient Project. All rights reserved.
 *
 * This program is licensed to you under the Apache License Version 2.0,
 * and you may not use this file except in compliance with the Apache License Version 2.0.
 * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Apache License Version 2.0 is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
 */
package org.asynchttpclient.uri;

import static org.asynchttpclient.util.Assertions.assertNotNull;
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;

final class UriParser {

  public String scheme;
  public String host;
  public int port = -1;
  public String query;
  public String fragment;
  private String authority;
  public String path;
  public String userInfo;

  private String originalUrl;
  private int start, end, currentIndex = 0;

  private void trimLeft() {
    while (start < end && originalUrl.charAt(start) <= ' ') {
      start++;
    }

    if (originalUrl.regionMatches(true, start, "url:", 0, 4)) {
      start += 4;
    }
  }

  private void trimRight() {
    end = originalUrl.length();
    while (end > 0 && originalUrl.charAt(end - 1) <= ' ') {
      end--;
    }
  }

  private boolean isFragmentOnly() {
    return start < originalUrl.length() && originalUrl.charAt(start) == '#';
  }

  private boolean isValidProtocolChar(char c) {
    return Character.isLetterOrDigit(c) && c != '.' && c != '+' && c != '-';
  }

  private boolean isValidProtocolChars(String protocol) {
    for (int i = 1; i < protocol.length(); i++) {
      if (!isValidProtocolChar(protocol.charAt(i))) {
        return false;
      }
    }
    return true;
  }

  private boolean isValidProtocol(String protocol) {
    return protocol.length() > 0 && Character.isLetter(protocol.charAt(0)) && isValidProtocolChars(protocol);
  }

  private void computeInitialScheme() {
    for (int i = currentIndex; i < end; i++) {
      char c = originalUrl.charAt(i);
      if (c == ':') {
        String s = originalUrl.substring(currentIndex, i);
        if (isValidProtocol(s)) {
          scheme = s.toLowerCase();
          currentIndex = i + 1;
        }
        break;
      } else if (c == '/') {
        break;
      }
    }
  }

  private boolean overrideWithContext(Uri context) {

    boolean isRelative = false;

    // use context only if schemes match
    if (context != null && (scheme == null || scheme.equalsIgnoreCase(context.getScheme()))) {

      // see RFC2396 5.2.3
      String contextPath = context.getPath();
      if (isNonEmpty(contextPath) && contextPath.charAt(0) == '/') {
        scheme = null;
      }

      if (scheme == null) {
        scheme = context.getScheme();
        userInfo = context.getUserInfo();
        host = context.getHost();
        port = context.getPort();
        path = contextPath;
        isRelative = true;
      }
    }
    return isRelative;
  }

  private int findWithinCurrentRange(char c) {
    int pos = originalUrl.indexOf(c, currentIndex);
    return pos > end ? -1 : pos;
  }

  private void trimFragment() {
    int charpPosition = findWithinCurrentRange('#');
    if (charpPosition >= 0) {
      end = charpPosition;
      if (charpPosition + 1 < originalUrl.length()) {
        fragment = originalUrl.substring(charpPosition + 1);
      }
    }
  }

  private void inheritContextQuery(Uri context, boolean isRelative) {
    // see RFC2396 5.2.2: query and fragment inheritance
    if (isRelative && currentIndex == end) {
      query = context.getQuery();
      fragment = context.getFragment();
    }
  }

  private boolean computeQuery() {
    if (currentIndex < end) {
      int askPosition = findWithinCurrentRange('?');
      if (askPosition != -1) {
        query = originalUrl.substring(askPosition + 1, end);
        if (end > askPosition) {
          end = askPosition;
        }
        return askPosition == currentIndex;
      }
    }
    return false;
  }

  private boolean currentPositionStartsWith4Slashes() {
    return originalUrl.regionMatches(currentIndex, "////", 0, 4);
  }

  private boolean currentPositionStartsWith2Slashes() {
    return originalUrl.regionMatches(currentIndex, "//", 0, 2);
  }

  private void computeAuthority() {
    int authorityEndPosition = findWithinCurrentRange('/');
    if (authorityEndPosition == -1) {
      authorityEndPosition = findWithinCurrentRange('?');
      if (authorityEndPosition == -1) {
        authorityEndPosition = end;
      }
    }
    host = authority = originalUrl.substring(currentIndex, authorityEndPosition);
    currentIndex = authorityEndPosition;
  }

  private void computeUserInfo() {
    int atPosition = authority.indexOf('@');
    if (atPosition != -1) {
      userInfo = authority.substring(0, atPosition);
      host = authority.substring(atPosition + 1);
    } else {
      userInfo = null;
    }
  }

  private boolean isMaybeIPV6() {
    // If the host is surrounded by [ and ] then its an IPv6
    // literal address as specified in RFC2732
    return host.length() > 0 && host.charAt(0) == '[';
  }

  private void computeIPV6() {
    int positionAfterClosingSquareBrace = host.indexOf(']') + 1;
    if (positionAfterClosingSquareBrace > 1) {

      port = -1;

      if (host.length() > positionAfterClosingSquareBrace) {
        if (host.charAt(positionAfterClosingSquareBrace) == ':') {
          // see RFC2396: port can be null
          int portPosition = positionAfterClosingSquareBrace + 1;
          if (host.length() > portPosition) {
            port = Integer.parseInt(host.substring(portPosition));
          }
        } else {
          throw new IllegalArgumentException("Invalid authority field: " + authority);
        }
      }

      host = host.substring(0, positionAfterClosingSquareBrace);

    } else {
      throw new IllegalArgumentException("Invalid authority field: " + authority);
    }
  }

  private void computeRegularHostPort() {
    int colonPosition = host.indexOf(':');
    port = -1;
    if (colonPosition >= 0) {
      // see RFC2396: port can be null
      int portPosition = colonPosition + 1;
      if (host.length() > portPosition)
        port = Integer.parseInt(host.substring(portPosition));
      host = host.substring(0, colonPosition);
    }
  }

  // /./
  private void removeEmbeddedDot() {
    path = path.replace("/./", "/");
  }

  // /../
  private void removeEmbedded2Dots() {
    int i = 0;
    while ((i = path.indexOf("/../", i)) >= 0) {
      if (i > 0) {
        end = path.lastIndexOf('/', i - 1);
        if (end >= 0 && path.indexOf("/../", end) != 0) {
          path = path.substring(0, end) + path.substring(i + 3);
          i = 0;
        } else if (end == 0) {
          break;
        }
      } else {
        i = i + 3;
      }
    }
  }

  private void removeTailing2Dots() {
    while (path.endsWith("/..")) {
      end = path.lastIndexOf('/', path.length() - 4);
      if (end >= 0) {
        path = path.substring(0, end + 1);
      } else {
        break;
      }
    }
  }

  private void removeStartingDot() {
    if (path.startsWith("./") && path.length() > 2) {
      path = path.substring(2);
    }
  }

  private void removeTrailingDot() {
    if (path.endsWith("/.")) {
      path = path.substring(0, path.length() - 1);
    }
  }

  private void handleRelativePath() {
    int lastSlashPosition = path.lastIndexOf('/');
    String pathEnd = originalUrl.substring(currentIndex, end);

    if (lastSlashPosition == -1) {
      path = authority != null ? "/" + pathEnd : pathEnd;
    } else {
      path = path.substring(0, lastSlashPosition + 1) + pathEnd;
    }
  }

  private void handlePathDots() {
    if (path.indexOf('.') != -1) {
      removeEmbeddedDot();
      removeEmbedded2Dots();
      removeTailing2Dots();
      removeStartingDot();
      removeTrailingDot();
    }
  }

  private void parseAuthority() {
    if (!currentPositionStartsWith4Slashes() && currentPositionStartsWith2Slashes()) {
      currentIndex += 2;

      computeAuthority();
      computeUserInfo();

      if (host != null) {
        if (isMaybeIPV6()) {
          computeIPV6();
        } else {
          computeRegularHostPort();
        }
      }

      if (port < -1) {
        throw new IllegalArgumentException("Invalid port number :" + port);
      }

      // see RFC2396 5.2.4: ignore context path if authority is defined
      if (isNonEmpty(authority)) {
        path = "";
      }
    }
  }

  private void computeRegularPath() {
    if (originalUrl.charAt(currentIndex) == '/') {
      path = originalUrl.substring(currentIndex, end);
    } else if (isNonEmpty(path)) {
      handleRelativePath();
    } else {
      String pathEnd = originalUrl.substring(currentIndex, end);
      path = isNonEmpty(pathEnd) && pathEnd.charAt(0) != '/' ? "/" + pathEnd : pathEnd;
    }
    handlePathDots();
  }

  private void computeQueryOnlyPath() {
    int lastSlashPosition = path.lastIndexOf('/');
    path = lastSlashPosition < 0 ? "/" : path.substring(0, lastSlashPosition) + "/";
  }

  private void computePath(boolean queryOnly) {
    // Parse the file path if any
    if (currentIndex < end) {
      computeRegularPath();
    } else if (queryOnly && path != null) {
      computeQueryOnlyPath();
    } else if (path == null) {
      path = "";
    }
  }

  public void parse(Uri context, final String originalUrl) {

    assertNotNull(originalUrl, "originalUrl");
    this.originalUrl = originalUrl;
    this.end = originalUrl.length();

    trimLeft();
    trimRight();
    currentIndex = start;
    if (!isFragmentOnly()) {
      computeInitialScheme();
    }
    boolean isRelative = overrideWithContext(context);
    trimFragment();
    inheritContextQuery(context, isRelative);
    boolean queryOnly = computeQuery();
    parseAuthority();
    computePath(queryOnly);
  }
}