/*
 * Copyright 2007-2011 Selenium committers
 * 
 * Licensed 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 io.selendroid.server.model;

import java.util.List;

import io.selendroid.server.model.internal.FindsByTagName;

/**
 * Mechanism used to locate elements within a document. In order to create your own locating
 * mechanisms, it is possible to subclass this class and override the protected methods as required,
 * though it is expected that that all subclasses rely on the basic finding mechanisms provided
 * through static methods of this class:
 * 
 * <code>
 * public WebElement findElement(WebDriver driver) {
 *     WebElement element = driver.findElement(By.id(getSelector()));
 *     if (element == null)
 *       element = driver.findElement(By.name(getSelector());
 *     return element;
 * }
 * </code>
 */
public abstract class By {
  public static class ByCssSelector extends By {
    private final String selector;

    public ByCssSelector(String selector) {
      this.selector = selector;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(By.cssSelector(selector));
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(By.cssSelector(selector));
    }

    @Override
    public String getElementLocator() {
      return selector;
    }
  }

  public static class ById extends By {
    private final String id;

    public ById(String id) {
      this.id = id;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(By.id(id));
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(By.id(id));
    }

    @Override
    public String getElementLocator() {
      return id;
    }

    @Override
    public String toString() {
      return "By.id: " + id;
    }
  }

  public static class ByClass extends By {
    private final String clazz;

    public ByClass(String clazz) {
      this.clazz = clazz;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(By.className(clazz));
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(By.className(clazz));
    }

    @Override
    public String getElementLocator() {
      return clazz;
    }

    @Override
    public String toString() {
      return "By.clazz: " + clazz;
    }
  }

  public static class ByTagName extends By {
    private final String tagName;

    public ByTagName(String tagName) {
      this.tagName = tagName;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return ((FindsByTagName) context).findElementByTagName(tagName);
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return ((FindsByTagName) context).findElementsByTagName((tagName));
    }

    @Override
    public String getElementLocator() {
      return tagName;
    }

    @Override
    public String toString() {
      return "By.tagName: " + tagName;
    }
  }

  public static class ByLinkText extends By {
    private final String text;

    public ByLinkText(String text) {
      this.text = text;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(By.linkText(text));
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(By.linkText(text));
    }

    @Override
    public String getElementLocator() {
      return text;
    }

    @Override
    public String toString() {
      return "By.text: " + text;
    }
  }

  public static class ByPartialLinkText extends By {
    private final String text;

    public ByPartialLinkText(String text) {
      this.text = text;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(By.partialLinkText(text));
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(By.partialLinkText(text));
    }

    @Override
    public String getElementLocator() {
      return text;
    }

    @Override
    public String toString() {
      return "By.partialText: " + text;
    }
  }

  public static class ByName extends By {
    private final String name;

    public ByName(String name) {
      this.name = name;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(this);
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(this);
    }

    @Override
    public String getElementLocator() {
      return name;
    }
  }

  public static class ByXPath extends By {
    private final String xpathExpression;

    public ByXPath(String xpathExpression) {
      this.xpathExpression = xpathExpression;
    }

    @Override
    public AndroidElement findElement(SearchContext context) {
      return context.findElement(this);
    }

    @Override
    public List<AndroidElement> findElements(SearchContext context) {
      return context.findElements(this);
    }

    @Override
    public String getElementLocator() {
      return xpathExpression;
    }
  }

  public static By cssSelector(String css) {
    if (css == null) throw new IllegalArgumentException("Cannot find elements when css is null.");
    return new ByCssSelector(css);
  }

  /**
   * @param id The value of the "id" attribute to search for
   * @return a By which locates elements by the value of the "id" attribute.
   */
  public static By id(final String id) {
    if (id == null)
      throw new IllegalArgumentException("Cannot find elements with a null id attribute.");

    return new ById(id);
  }

  public static By tagName(final String tagName) {
    if (tagName == null)
      throw new IllegalArgumentException("Cannot find elements when tag name is null.");

    return new ByTagName(tagName);
  }

  public static By linkText(final String text) {
    if (text == null)
      throw new IllegalArgumentException("Cannot find elements when text is null.");

    return new ByLinkText(text);
  }

  public static By partialLinkText(final String text) {
    if (text == null)
      throw new IllegalArgumentException("Cannot find elements when text is null.");

    return new ByPartialLinkText(text);
  }

  public static By name(String name) {
    if (name == null)
      throw new IllegalArgumentException("Cannot find elements when name is null.");
    return new ByName(name);
  }

  public static By xpath(String xpathExpression) {
    if (xpathExpression == null)
      throw new IllegalArgumentException("Cannot find elements when xpath is null.");
    return new ByXPath(xpathExpression);
  }

  public static By className(String className) {
    if (className == null)
      throw new IllegalArgumentException("Cannot find elements when className is null.");
    return new ByClass(className);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    By by = (By) o;

    return toString().equals(by.toString());
  }

  /**
   * Find a single element. Override this method if necessary.
   * 
   * @param context A context to use to find the element
   * @return The AndroidElement that matches the selector
   */
  public abstract AndroidElement findElement(SearchContext context);

  /**
   * Find many elements.
   * 
   * @param context A context to use to find the element
   * @return A list of AndroidElement matching the selector
   */
  public abstract List<AndroidElement> findElements(SearchContext context);

  public abstract String getElementLocator();

  @Override
  public int hashCode() {
    return toString().hashCode();
  }

  @Override
  public String toString() {
    // A stub to prevent endless recursion in hashCode()
    return "[unknown locator]";
  }
}
