/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datastax.driver.mapping;

import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;

/** Implementations of industry common naming conventions. */
public class NamingConventions {

  /**
   * Represents a naming convention where all letters are lower cased, and words are not separated
   * by any special character. E.g. "myxmlparser".
   */
  public static final NamingConvention LOWER_CASE = new SingleWordNamingConvention(false);

  /**
   * Represents a naming convention where all letters are upper cased, and words are not separated
   * by any special character. E.g. "MYXMLPARSER".
   */
  public static final NamingConvention UPPER_CASE = new SingleWordNamingConvention(true);

  /**
   * Represents <a href="https://en.wikipedia.org/wiki/Snake_case">snake case</a> naming convention,
   * meaning all letters are lower cased, and words are separated by an underscore ("_"). E.g.
   * "my_xml_parser".
   */
  public static final NamingConvention LOWER_SNAKE_CASE =
      new CharDelimitedNamingConvention("_", false);

  /**
   * Represents <a href="https://en.wikipedia.org/wiki/Snake_case">snake case</a> naming convention,
   * meaning all letters are upper cased, and words are separated by an underscore ("_"). E.g.
   * "MY_XML_PARSER".
   */
  public static final NamingConvention UPPER_SNAKE_CASE =
      new CharDelimitedNamingConvention("_", true);

  /**
   * Represents <a href="https://en.wikipedia.org/wiki/Letter_case#Special_case_styles">Lisp
   * case</a> naming convention, meaning all letters are lower cased, and words are separated by a
   * dash sign ("-"). E.g. "my-xml-parser"
   */
  public static final NamingConvention LOWER_LISP_CASE =
      new CharDelimitedNamingConvention("-", false);

  /**
   * Represents <a href="https://en.wikipedia.org/wiki/Letter_case#Special_case_styles">Lisp
   * case</a> naming convention, meaning all letters are upper cased, and words are separated by a
   * dash sign ("-"). E.g. "MY-XML-PARSER".
   */
  public static final NamingConvention UPPER_LISP_CASE =
      new CharDelimitedNamingConvention("-", true);

  /**
   * Represents the default <a href="https://en.wikipedia.org/wiki/Camel_case">Camel case</a> naming
   * convention, with a lower cased first letter.
   *
   * @see LowerCamelCase
   */
  public static final NamingConvention LOWER_CAMEL_CASE = new LowerCamelCase();

  /**
   * Represents the default <a href="https://en.wikipedia.org/wiki/Camel_case">Camel case</a> naming
   * convention, with an upper cased first letter.
   *
   * @see UpperCamelCase
   */
  public static final NamingConvention UPPER_CAMEL_CASE = new UpperCamelCase();

  /**
   * Represents <a href="https://en.wikipedia.org/wiki/Camel_case">Camel case</a> naming convention
   * with a lower cased first letter.
   *
   * <p>E.g. "myXmlParser" and "myXMLParser". Note that both examples are valid lower camel case
   * forms. The first one takes abbreviations as any other words where the first letter is upper
   * case, and the rest are lower case (hence - "Xml"), while the latter upper cases all letters of
   * an abbreviation (hence - "XML").
   *
   * <p>Additionally, many different Java naming conventions introduce prefixes for field naming,
   * some examples:
   *
   * <ul>
   *   <li><a
   *       href="http://source.android.com/source/code-style.html#follow-field-naming-conventions">Android</a>
   *   <li><a href="https://en.wikipedia.org/wiki/Hungarian_notation">Hungarian Notation</a>
   *   <li><a
   *       href="http://stackoverflow.com/questions/1899683/is-there-a-standard-in-java-for-underscore-in-front-of-variable-or-class-nam">Underscore</a>
   * </ul>
   *
   * Those prefixes can be supported. For example, if this convention is configured with {@code
   * ignorablePrefixes} set to "_" then a field named "_myXmlParser" will be split in 3 words only:
   * "my", "Xml", "Parser".
   */
  public static class LowerCamelCase extends CamelCase {

    private final boolean upperCaseAbbreviations;

    /**
     * @param upperCaseAbbreviations {@code true} to uppercase all abbreviations, {@code false} to
     *     treat abbreviations as any other word
     * @param ignorablePrefixes string prefixes to trim if constant field name prefixes are used
     */
    public LowerCamelCase(boolean upperCaseAbbreviations, String... ignorablePrefixes) {
      super(ignorablePrefixes);
      this.upperCaseAbbreviations = upperCaseAbbreviations;
    }

    /** @param ignorablePrefixes string prefixes to trim if constant field name prefixes are used */
    public LowerCamelCase(String... ignorablePrefixes) {
      this(false, ignorablePrefixes);
    }

    /**
     * @param upperCaseAbbreviations {@code true} to uppercase all abbreviations, {@code false} to
     *     treat abbreviations as any other word
     */
    public LowerCamelCase(boolean upperCaseAbbreviations) {
      this(upperCaseAbbreviations, new String[0]);
    }

    @Override
    public String join(List<Word> input) {
      StringBuilder builder = new StringBuilder();
      for (int i = 0; i < input.size(); i++) {
        Word word = input.get(i);
        String value;
        if (i == 0) {
          value = word.getValue().toLowerCase();
        } else if (upperCaseAbbreviations && word.isAbbreviation()) {
          value = word.getValue().toUpperCase();
        } else {
          value =
              word.getValue().substring(0, 1).toUpperCase()
                  + word.getValue().substring(1).toLowerCase();
        }
        builder.append(value);
      }
      return builder.toString();
    }
  }

  /**
   * Represents <a href="https://en.wikipedia.org/wiki/Camel_case">Camel case</a> naming convention
   * with an upper cased first letter.
   *
   * <p>E.g. "MyXmlParser" and "MyXMLParser". Note that both examples are valid upper camel case
   * forms. The first one takes abbreviations as any other words where the first letter is upper
   * case, and the rest are lower case (hence - "Xml"), while the latter upper cases all letters of
   * an abbreviation (hence - "XML").
   *
   * <p>Additionally, many different Java naming conventions introduce prefixes for field naming,
   * some examples:
   *
   * <ul>
   *   <li><a
   *       href="http://source.android.com/source/code-style.html#follow-field-naming-conventions">Android</a>
   *   <li><a href="https://en.wikipedia.org/wiki/Hungarian_notation">Hungarian Notation</a>
   *   <li><a
   *       href="http://stackoverflow.com/questions/1899683/is-there-a-standard-in-java-for-underscore-in-front-of-variable-or-class-nam">Underscore</a>
   * </ul>
   *
   * Those prefixes can be supported. For example, if this convention is configured with {@code
   * ignorablePrefixes} set to "_" then a field named "_MyXmlParser" will be split in 3 words only:
   * "My", "Xml", "Parser".
   */
  public static class UpperCamelCase extends CamelCase {

    private final boolean upperCaseAbbreviations;

    /**
     * @param upperCaseAbbreviations {@code true} to uppercase all abbreviations, {@code false} to
     *     treat abbreviations as any other word
     * @param ignorablePrefixes string prefixes to trim if constant field name prefixes are used
     */
    public UpperCamelCase(boolean upperCaseAbbreviations, String... ignorablePrefixes) {
      super(ignorablePrefixes);
      this.upperCaseAbbreviations = upperCaseAbbreviations;
    }

    /** @param ignorablePrefixes string prefixes to trim if constant field name prefixes are used */
    public UpperCamelCase(String... ignorablePrefixes) {
      this(false, ignorablePrefixes);
    }

    /**
     * @param upperCaseAbbreviations {@code true} to uppercase all abbreviations, {@code false} to
     *     treat abbreviations as any other word
     */
    public UpperCamelCase(boolean upperCaseAbbreviations) {
      this(upperCaseAbbreviations, new String[0]);
    }

    @Override
    public String join(List<Word> input) {
      StringBuilder builder = new StringBuilder();
      for (Word word : input) {
        String value;
        if (upperCaseAbbreviations && word.isAbbreviation()) {
          value = word.getValue().toUpperCase();
        } else {
          value =
              word.getValue().substring(0, 1).toUpperCase()
                  + word.getValue().substring(1).toLowerCase();
        }
        builder.append(value);
      }
      return builder.toString();
    }
  }

  public abstract static class CamelCase implements NamingConvention {

    private static final Pattern SPLIT_PATTERN =
        Pattern.compile(
            String.format(
                "%s|%s|%s",
                "(?<=[A-Z])(?=[A-Z][a-z])", "(?<=[^A-Z])(?=[A-Z])", "(?<=[A-Za-z])(?=[^A-Za-z])"));

    private static final Pattern ALL_UPPERCASE_PATTERN = Pattern.compile("([A-Z])*");

    private static final Comparator<String> STRING_LENGTH_COMPARATOR =
        new Comparator<String>() {
          @Override
          public int compare(String o1, String o2) {
            return o2.length() - o1.length();
          }
        };

    private final Pattern ignorablePrefixPattern;

    protected CamelCase(String... ignorablePrefixes) {
      Arrays.sort(ignorablePrefixes, STRING_LENGTH_COMPARATOR);
      StringBuilder builder = new StringBuilder();
      for (int i = 0; i < ignorablePrefixes.length; i++) {
        if (i > 0) {
          builder.append("|");
        }
        builder.append("^");
        builder.append(ignorablePrefixes[i]);
      }
      ignorablePrefixPattern = Pattern.compile(builder.toString());
    }

    @Override
    public List<Word> split(String input) {
      List<Word> result = new LinkedList<Word>();
      // slice all ignorable prefixes, then split
      for (String value :
          SPLIT_PATTERN.split(ignorablePrefixPattern.matcher(input).replaceAll(""))) {
        // if all uppercase, mark as abbreviation (e.g. MyXMLParser)
        boolean isAbbreviation = ALL_UPPERCASE_PATTERN.matcher(value).matches();
        Word word = new Word(value, isAbbreviation);
        result.add(word);
      }
      return result;
    }
  }

  public static class CharDelimitedNamingConvention implements NamingConvention {

    private final String delimiter;

    private final boolean isUpperCase;

    protected CharDelimitedNamingConvention(String delimiter, boolean isUpperCase) {
      this.delimiter = delimiter;
      this.isUpperCase = isUpperCase;
    }

    @Override
    public List<Word> split(String input) {
      List<Word> result = new LinkedList<Word>();
      for (String value : input.split(delimiter)) {
        result.add(new Word(value));
      }
      return result;
    }

    @Override
    public String join(List<Word> input) {
      StringBuilder builder = new StringBuilder();
      for (int i = 0; i < input.size(); i++) {
        if (i > 0) {
          builder.append(delimiter);
        }
        builder.append(input.get(i).getValue());
      }
      String result = builder.toString();
      return isUpperCase ? result.toUpperCase() : result.toLowerCase();
    }
  }

  public static class SingleWordNamingConvention implements NamingConvention {

    private final boolean isUpperCase;

    protected SingleWordNamingConvention(boolean isUpperCase) {
      this.isUpperCase = isUpperCase;
    }

    @Override
    public List<Word> split(String input) {
      List<Word> result = new LinkedList<Word>();
      result.add(new Word(input));
      return result;
    }

    @Override
    public String join(List<Word> input) {
      StringBuilder builder = new StringBuilder();
      for (Word word : input) {
        builder.append(word.getValue());
      }
      String result = builder.toString();
      return isUpperCase ? result.toUpperCase() : result.toLowerCase();
    }
  }
}
