/**
 * boilerpipe
 *
 * Copyright (c) 2009, 2014 Christian Kohlschütter
 *
 * The author 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.kohlschutter.boilerpipe.sax;

import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.xerces.parsers.AbstractSAXParser;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.cyberneko.html.HTMLConfiguration;

import com.kohlschutter.boilerpipe.BoilerpipeExtractor;
import com.kohlschutter.boilerpipe.BoilerpipeProcessingException;
import com.kohlschutter.boilerpipe.document.Image;
import com.kohlschutter.boilerpipe.document.TextBlock;
import com.kohlschutter.boilerpipe.document.TextDocument;

/**
 * Extracts the images that are enclosed by extracted content.
 */
public final class ImageExtractor {
  public static final ImageExtractor INSTANCE = new ImageExtractor();

  /**
   * Returns the singleton instance of {@link ImageExtractor}.
   * 
   * @return
   */
  public static ImageExtractor getInstance() {
    return INSTANCE;
  }

  private ImageExtractor() {
  }

  /**
   * Processes the given {@link TextDocument} and the original HTML text (as a String).
   * 
   * @param doc The processed {@link TextDocument}.
   * @param origHTML The original HTML document.
   * @return A List of enclosed {@link Image}s
   * @throws BoilerpipeProcessingException
   */
  public List<Image> process(final TextDocument doc, final String origHTML)
      throws BoilerpipeProcessingException {
    return process(doc, new InputSource(new StringReader(origHTML)));
  }

  /**
   * Processes the given {@link TextDocument} and the original HTML text (as an {@link InputSource}
   * ).
   * 
   * @param doc The processed {@link TextDocument}.
   * @param origHTML The original HTML document.
   * @return A List of enclosed {@link Image}s
   * @throws BoilerpipeProcessingException
   */
  public List<Image> process(final TextDocument doc, final InputSource is)
      throws BoilerpipeProcessingException {
    final Implementation implementation = new Implementation();
    implementation.process(doc, is);

    return implementation.linksHighlight;
  }

  /**
   * Fetches the given {@link URL} using {@link HTMLFetcher} and processes the retrieved HTML using
   * the specified {@link BoilerpipeExtractor}.
   * 
   * @param doc The processed {@link TextDocument}.
   * @param is The original HTML document.
   * @return A List of enclosed {@link Image}s
   * @throws BoilerpipeProcessingException
   */
  public List<Image> process(final URL url, final BoilerpipeExtractor extractor)
      throws IOException, BoilerpipeProcessingException, SAXException {
    final HTMLDocument htmlDoc = HTMLFetcher.fetch(url);

    final TextDocument doc = new BoilerpipeSAXInput(htmlDoc.toInputSource()).getTextDocument();
    extractor.process(doc);

    final InputSource is = htmlDoc.toInputSource();

    return process(doc, is);
  }

  private final class Implementation extends AbstractSAXParser implements ContentHandler {
    List<Image> linksHighlight = new ArrayList<Image>();
    private List<Image> linksBuffer = new ArrayList<Image>();

    private int inIgnorableElement = 0;
    private int characterElementIdx = 0;
    private final BitSet contentBitSet = new BitSet();

    private boolean inHighlight = false;

    Implementation() {
      super(new HTMLConfiguration());
      setContentHandler(this);
    }

    void process(final TextDocument doc, final InputSource is) throws BoilerpipeProcessingException {
      for (TextBlock block : doc.getTextBlocks()) {
        if (block.isContent()) {
          final BitSet bs = block.getContainedTextElements();
          if (bs != null) {
            contentBitSet.or(bs);
          }
        }
      }

      try {
        parse(is);
      } catch (SAXException e) {
        throw new BoilerpipeProcessingException(e);
      } catch (IOException e) {
        throw new BoilerpipeProcessingException(e);
      }
    }

    public void endDocument() throws SAXException {
    }

    public void endPrefixMapping(String prefix) throws SAXException {
    }

    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
    }

    public void processingInstruction(String target, String data) throws SAXException {
    }

    public void setDocumentLocator(Locator locator) {
    }

    public void skippedEntity(String name) throws SAXException {
    }

    public void startDocument() throws SAXException {
    }

    public void startElement(String uri, String localName, String qName, Attributes atts)
        throws SAXException {
      TagAction ta = TAG_ACTIONS.get(localName);
      if (ta != null) {
        ta.beforeStart(this, localName);
      }

      try {
        if (inIgnorableElement == 0) {
          if (inHighlight && "IMG".equalsIgnoreCase(localName)) {
            String src = atts.getValue("src");
            if (src != null && src.length() > 0) {
              linksBuffer.add(new Image(src, atts.getValue("width"), atts.getValue("height"), atts
                  .getValue("alt")));
            }
          }
        }
      } finally {
        if (ta != null) {
          ta.afterStart(this, localName);
        }
      }
    }

    public void endElement(String uri, String localName, String qName) throws SAXException {
      TagAction ta = TAG_ACTIONS.get(localName);
      if (ta != null) {
        ta.beforeEnd(this, localName);
      }

      try {
        if (inIgnorableElement == 0) {
          //
        }
      } finally {
        if (ta != null) {
          ta.afterEnd(this, localName);
        }
      }
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
      characterElementIdx++;
      if (inIgnorableElement == 0) {

        boolean highlight = contentBitSet.get(characterElementIdx);
        if (!highlight) {
          if (length == 0) {
            return;
          }
          boolean justWhitespace = true;
          for (int i = start; i < start + length; i++) {
            if (!Character.isWhitespace(ch[i])) {
              justWhitespace = false;
              break;
            }
          }
          if (justWhitespace) {
            return;
          }
        }

        inHighlight = highlight;
        if (inHighlight) {
          linksHighlight.addAll(linksBuffer);
          linksBuffer.clear();
        }
      }
    }

    public void startPrefixMapping(String prefix, String uri) throws SAXException {
    }

  }

  private static final TagAction TA_IGNORABLE_ELEMENT = new TagAction() {
    void beforeStart(final Implementation instance, final String localName) {
      instance.inIgnorableElement++;
    }

    void afterEnd(final Implementation instance, final String localName) {
      instance.inIgnorableElement--;
    }
  };

  private static Map<String, TagAction> TAG_ACTIONS = new HashMap<String, TagAction>();
  static {
    TAG_ACTIONS.put("STYLE", TA_IGNORABLE_ELEMENT);
    TAG_ACTIONS.put("SCRIPT", TA_IGNORABLE_ELEMENT);
    TAG_ACTIONS.put("OPTION", TA_IGNORABLE_ELEMENT);
    TAG_ACTIONS.put("NOSCRIPT", TA_IGNORABLE_ELEMENT);
    TAG_ACTIONS.put("EMBED", TA_IGNORABLE_ELEMENT);
    TAG_ACTIONS.put("APPLET", TA_IGNORABLE_ELEMENT);
    TAG_ACTIONS.put("LINK", TA_IGNORABLE_ELEMENT);

    TAG_ACTIONS.put("HEAD", TA_IGNORABLE_ELEMENT);
  }

  private abstract static class TagAction {
    void beforeStart(final Implementation instance, final String localName) {
    }

    void afterStart(final Implementation instance, final String localName) {
    }

    void beforeEnd(final Implementation instance, final String localName) {
    }

    void afterEnd(final Implementation instance, final String localName) {
    }
  }
}
