package com.iig.ziawe.gui;

import com.iig.ziawe.ZiaweCore;
import com.iig.ziawe.actions.events.ZiaweDocumentEvent;
import com.iig.ziawe.block.Block;

import com.iig.ziawe.block.FormattedText;
import com.iig.ziawe.block.RootElementWrapper;
import com.iig.ziawe.utils.Util;

import java.awt.Color;
import java.awt.Font;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.TextAttribute;

import java.io.Serializable;

import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.GlyphView;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import javax.swing.text.StateInvariantError;
import javax.swing.text.StringContent;
import javax.swing.text.Style;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;

import javax.swing.text.View;
import javax.swing.text.ViewFactory;

import sun.swing.SwingUtilities2;

public class ZiaweDocument implements StyledDocument, Serializable {

  RootElementWrapper root;
  StyleContext context;
  ZiaweCore core;
  BlockContent content;
  EventListenerList documentListeners;
  EventListenerList undoableListeners;
  private Dictionary<Object, Object> documentProperties = null;

  private transient int numReaders;
  private transient Thread currWriter;

  private static final String BAD_LOCK_STATE = "document lock failure";

  /**
   * Document property that indicates whether internationalization
   * functions such as text reordering or reshaping should be
   * performed. This property should not be publicly exposed,
   * since it is used for implementation convenience only.  As a 
   * side effect, copies of this property may be in its subclasses
   * that live in different packages (e.g. HTMLDocument as of now),
   * so those copies should also be taken care of when this property
   * needs to be modified.
   */
  static final String I18NProperty = "i18n";

  /**
   * Document property that indicates if a character has been inserted
   * into the document that is more than one byte long.  GlyphView uses
   * this to determine if it should use BreakIterator.
   */
  static final Object MultiByteProperty = "multiByte";

  public ZiaweDocument() {

    FormattedText defaultRoot = 
      new FormattedText(" ");

    //jetzt in aktion bei Mc donalds
    RootElementWrapper wrap = new RootElementWrapper(defaultRoot);

    StyleContext def = StyleContext.getDefaultStyleContext();
    wrap.setElemParams(null, new Vector(), 
                       def.getStyle(StyleContext.DEFAULT_STYLE), 0, this);

    core = ZiaweCore.getInstance();

    core.getBlockList().add(wrap);
    this.content = new BlockContent(core.getBlockList());
    this.root = wrap;
    this.context = def;
    this.putProperty(this.I18NProperty, false);
    this.putProperty(this.MultiByteProperty, false);
    this.getStartPosition();
    GlyphView v;
  }

  public Style addStyle(String nm, Style parent) {
    return context.addStyle(nm, parent);
  }

  public void removeStyle(String nm) {
    context.removeStyle(nm);
  }

  public Style getStyle(String nm) {
    return context.getStyle(nm);
  }

  public void setCharacterAttributes(int offset, int length, AttributeSet s, 
                                     boolean replace) {
    FormattedText fm = this.getFormattedTextFromPosition(offset);
    if (fm != null) {
      fm.addAttributes(s);
    }
    //TODO selection formatieren und nicht gesamten block
  }

  public void setParagraphAttributes(int offset, int length, AttributeSet s, 
                                     boolean replace) {
    //TODO Step 1: implement code
    //TODO Step 2: ???
    //TODO Step 3: Profit
    this.setCharacterAttributes(offset, length, s, replace);
  }

  public void setLogicalStyle(int pos, Style s) {
    //TODO ???
    setCharacterAttributes(pos, 0, s, true);
  }

  public Style getLogicalStyle(int p) {
    FormattedText fm = this.getFormattedTextFromPosition(p);
    if (fm != null) {
      return (Style) fm.getResolveParent();
    }
    return null;
  }

  public Element getParagraphElement(int pos) {
    return this.getFormattedTextFromPosition(pos);
  }

  public Element getCharacterElement(int pos) {
    return this.getParagraphElement(pos);
  }

  public Color getForeground(AttributeSet attr) {
    return context.getForeground(attr);
  }

  public Color getBackground(AttributeSet attr) {
    return context.getBackground(attr);
  }

  public Font getFont(AttributeSet attr) {
    return context.getFont(attr);
  }

  public int getLength() {
    return content.length();
  }

  public void addDocumentListener(DocumentListener listener) {
    if (this.documentListeners == null) {
      documentListeners = new EventListenerList();
    }
    Object[] list = documentListeners.getListenerList();
    if ((list.length>0)&&(list[list.length - 1] instanceof ZiaweEditor)) {
      documentListeners.remove(DocumentListener.class, 
                               (DocumentListener) list[list.length - 1]);
      documentListeners.add(DocumentListener.class, listener);
      documentListeners.add(DocumentListener.class, 
                            (DocumentListener) list[list.length - 1]);
    } else {
      this.documentListeners.add(DocumentListener.class, listener);
    }
  }

  public void removeDocumentListener(DocumentListener listener) {
    if (this.documentListeners.getListenerCount() != 0) {
      documentListeners.remove(DocumentListener.class, listener);
    }
  }

  public void addUndoableEditListener(UndoableEditListener listener) {
    if (this.undoableListeners == null) {
      this.undoableListeners = new EventListenerList();
    }
    this.undoableListeners.add(UndoableEditListener.class, listener);
  }

  public void removeUndoableEditListener(UndoableEditListener listener) {
    if (this.undoableListeners.getListenerCount() != 0) {
      undoableListeners.remove(UndoableEditListener.class, listener);
    }
  }

  public Object getProperty(Object key) {
    return this.documentProperties.get(key);
  }

  public void putProperty(Object key, Object value) {
    if (value != null) {
      this.getDocumentProperties().put(key, value);
    }
  }

  public void remove(int offs, int len) {
    try {
      FormattedText fm = this.getFormattedTextFromPosition(offs);
      ZiaweDocumentEvent event = 
        new ZiaweDocumentEvent(offs, len, DocumentEvent.EventType.REMOVE, 
                               this);
      if (fm != null) {
        content.remove(offs, len);
        fm.calculateNewOffsets();
        this.fireRemoveUpdate(event);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  public void insertString(int offset, String str, AttributeSet a) {
    if (str == null) {
      return;
    }
    FormattedText fm = this.getFormattedTextFromPosition(offset);
    ZiaweDocumentEvent event = 
      new ZiaweDocumentEvent(offset, str.length(), DocumentEvent.EventType.INSERT, 
                             this);
    if (fm != null) {
      fm.addAttributes(a);
      try {

        content.insertString(offset, str);
        fm.calculateNewOffsets(fm.getStartOffset());
        this.getEndPosition();

        if (getProperty(I18NProperty).equals(Boolean.FALSE)) {
          // if a default direction of right-to-left has been specified,
          // we want complex layout even if the text is all left to right.
          Object d = getProperty(TextAttribute.RUN_DIRECTION);
          if ((d != null) && (d.equals(TextAttribute.RUN_DIRECTION_RTL))) {
            putProperty(I18NProperty, Boolean.TRUE);
          } else {
            char[] chars = str.toCharArray();
            if (SwingUtilities2.isComplexLayout(chars, 0, chars.length)) {
              putProperty(I18NProperty, Boolean.TRUE);
            }
          }
        }

        insertUpdate(event, a);
        fireInsertUpdate(event);

        System.out.println("INSERT STRING BLOCKLIST");
        Util.printBlockList();
      } catch (BadLocationException e) {
        e.printStackTrace();
      }
    }
  }

  public String getText(int offset, int length) {
    try {
      return content.getString(offset, length);
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
    return null;
  }

  public void getText(int offset, int length, Segment txt) {
    try {
      content.getChars(offset, length, txt);
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  public Position getStartPosition() {

    return content.createPosition(0);

  }

  public Position createPosition(int offs) {

    return content.createPosition(offs);

  }

  public Element[] getRootElements() {
    return new Element[] { this.root };
  }

  public Element getDefaultRootElement() {
    return this.root;
  }

  public void render(Runnable r) {
    r.run();
  }

  private FormattedText getFormattedTextFromPosition(int i) {
    Block blck = Util.findBlock(i, core.getBlockList()).getBlock();
    if (blck instanceof FormattedText) {
      return (FormattedText) blck;
    }
    return null;
  }

  // ----------------------FROM DEFAULTSTYLED / ABSTRACTDOCUMENT----------

  public Dictionary<Object, Object> getDocumentProperties() {
    if (documentProperties == null) {
      documentProperties = new Hashtable(2);
    }
    return documentProperties;
  }

  protected void fireInsertUpdate(DocumentEvent e) {

    // Guaranteed to return a non-null array
    Object[] listeners = this.documentListeners.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == DocumentListener.class) {
        // Lazily create the event:
        // if (e == null)
        // e = new ListSelectionEvent(this, firstIndex, lastIndex);
        ((DocumentListener) listeners[i + 1]).insertUpdate(e);
      }
    }

  }

  protected void fireChangedUpdate(DocumentEvent e) {

    // Guaranteed to return a non-null array
    Object[] listeners = this.documentListeners.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == DocumentListener.class) {
        // Lazily create the event:
        // if (e == null)
        // e = new ListSelectionEvent(this, firstIndex, lastIndex);
        ((DocumentListener) listeners[i + 1]).changedUpdate(e);
      }
    }
  }

  protected void fireRemoveUpdate(DocumentEvent e) {
    // Guaranteed to return a non-null array
    Object[] listeners = this.documentListeners.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == DocumentListener.class) {
        // Lazily create the event:
        // if (e == null)
        // e = new ListSelectionEvent(this, firstIndex, lastIndex);
        ((DocumentListener) listeners[i + 1]).removeUpdate(e);
      }
    }
  }

  public Position getEndPosition() {

    return content.getEndPosition();

  }

  /**
   * Updates document structure as a result of text insertion.  This
   * will happen within a write lock.  If a subclass of
   * this class reimplements this method, it should delegate to the
   * superclass as well.
   *
   * @param chng a description of the change
   * @param attr the attributes for the change
   */
  protected void insertUpdate(ZiaweDocumentEvent chng, AttributeSet attr) {

    // Check if a multi byte is encountered in the inserted text.
    if (chng.getType() == DocumentEvent.EventType.INSERT && 
        chng.getLength() > 0 && 
        !Boolean.TRUE.equals(getProperty(MultiByteProperty))) {
      Segment segment = new Segment();

      getText(chng.getOffset(), chng.getLength(), segment);
      segment.first();
      do {
        if ((int) segment.current() > 255) {
          putProperty(MultiByteProperty, Boolean.TRUE);
          break;
        }
      } while (segment.next() != Segment.DONE);

    }
  }

  /**
   * Acquires a lock to begin reading some state from the 
   * document.  There can be multiple readers at the same time.
   * Writing blocks the readers until notification of the change
   * to the listeners has been completed.  This method should
   * be used very carefully to avoid unintended compromise
   * of the document.  It should always be balanced with a
   * <code>readUnlock</code>.
   *
   * @see #readUnlock
   */
  public final synchronized void readLock() {
    try {
      while (currWriter != null) {
        if (currWriter == Thread.currentThread()) {
          // writer has full read access.... may try to acquire
          // lock in notification
          return;
        }
        wait();
      }
      numReaders += 1;
    } catch (InterruptedException e) {
      throw new Error("Interrupted attempt to aquire read lock");
    }
  }

  /**
   * Does a read unlock.  This signals that one
   * of the readers is done.  If there are no more readers
   * then writing can begin again.  This should be balanced
   * with a readLock, and should occur in a finally statement
   * so that the balance is guaranteed.  The following is an
   * example.
   * <pre><code>
   * &nbsp;   readLock();
   * &nbsp;   try {
   * &nbsp;       // do something
   * &nbsp;   } finally {
   * &nbsp;       readUnlock();
   * &nbsp;   }
   * </code></pre>
   *
   * @see #readLock
   */
  public final synchronized void readUnlock() {
    if (currWriter == Thread.currentThread()) {
      // writer has full read access.... may try to acquire
      // lock in notification
      return;
    }
    if (numReaders <= 0) {
      throw new StateInvariantError(BAD_LOCK_STATE);
    }
    numReaders -= 1;
    notify();
  }

  class StateInvariantError extends Error {
    /**
     * Creates a new StateInvariantFailure object.
     *
     * @param s   a string indicating the assertion that failed
     */
    public StateInvariantError(String s) {
      super(s);
    }

  }
}
