/*
 * 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.
 */

/* $Id$ */

package org.apache.fop.accessibility.fo;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import org.xml.sax.SAXException;

import org.apache.fop.accessibility.Accessibility;
import org.apache.fop.accessibility.StructureTreeEventHandler;
import org.apache.fop.fo.DelegatingFOEventHandler;
import org.apache.fop.fo.FOEventHandler;
import org.apache.fop.fo.FOText;
import org.apache.fop.fo.extensions.ExternalDocument;
import org.apache.fop.fo.flow.AbstractRetrieveMarker;
import org.apache.fop.fo.flow.BasicLink;
import org.apache.fop.fo.flow.Block;
import org.apache.fop.fo.flow.BlockContainer;
import org.apache.fop.fo.flow.Character;
import org.apache.fop.fo.flow.ExternalGraphic;
import org.apache.fop.fo.flow.Footnote;
import org.apache.fop.fo.flow.FootnoteBody;
import org.apache.fop.fo.flow.Inline;
import org.apache.fop.fo.flow.InstreamForeignObject;
import org.apache.fop.fo.flow.Leader;
import org.apache.fop.fo.flow.ListBlock;
import org.apache.fop.fo.flow.ListItem;
import org.apache.fop.fo.flow.ListItemBody;
import org.apache.fop.fo.flow.ListItemLabel;
import org.apache.fop.fo.flow.PageNumber;
import org.apache.fop.fo.flow.PageNumberCitation;
import org.apache.fop.fo.flow.PageNumberCitationLast;
import org.apache.fop.fo.flow.RetrieveMarker;
import org.apache.fop.fo.flow.RetrieveTableMarker;
import org.apache.fop.fo.flow.Wrapper;
import org.apache.fop.fo.flow.table.Table;
import org.apache.fop.fo.flow.table.TableBody;
import org.apache.fop.fo.flow.table.TableCell;
import org.apache.fop.fo.flow.table.TableColumn;
import org.apache.fop.fo.flow.table.TableFooter;
import org.apache.fop.fo.flow.table.TableHeader;
import org.apache.fop.fo.flow.table.TableRow;
import org.apache.fop.fo.pagination.Flow;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.pagination.Root;
import org.apache.fop.fo.pagination.StaticContent;
import org.apache.fop.fo.properties.CommonAccessibility;
import org.apache.fop.fo.properties.CommonAccessibilityHolder;

/**
 * Allows to create the structure tree of an FO document, by converting FO
 * events into appropriate structure tree events.
 */
public class FO2StructureTreeConverter extends DelegatingFOEventHandler {

    /** The top of the {@link converters} stack. */
    protected FOEventHandler converter;

    private Stack<FOEventHandler> converters = new Stack<FOEventHandler>();

    private final StructureTreeEventTrigger structureTreeEventTrigger;

    /** The descendants of some elements like fo:leader must be ignored. */
    private final FOEventHandler eventSwallower = new FOEventHandler() {
    };

    private final Map<AbstractRetrieveMarker, State> states = new HashMap<AbstractRetrieveMarker, State>();

    private static final class State {

        private final FOEventHandler converter;

        private final Stack<FOEventHandler> converters;

        @SuppressWarnings("unchecked")
        State(FO2StructureTreeConverter o) {
            this.converter = o.converter;
            this.converters = (Stack<FOEventHandler>) o.converters.clone();
        }

    }

    private Event root = new Event((Event) null);
    private Event currentNode = root;

    private void startContent(Event event, boolean hasContent) {
        if (getUserAgent().isKeepEmptyTags()) {
            event.run();
        } else {
            Event node = new Event(currentNode);
            event.hasContent = hasContent;
            node.add(event);
            currentNode.add(node);
            currentNode = node;
        }
    }

    private void content(Event event, boolean hasContent) {
        if (getUserAgent().isKeepEmptyTags()) {
            event.run();
        } else {
            currentNode.add(event);
            event.hasContent = hasContent;
        }
    }

    private void endContent(Event event) {
        if (getUserAgent().isKeepEmptyTags()) {
            event.run();
        } else {
            currentNode.add(event);
            currentNode = currentNode.parent;
            if (currentNode == root) {
                root.run();
            }
        }
    }

    /**
     * Creates a new instance.
     *
     * @param structureTreeEventHandler the object that will hold the structure tree
     * @param delegate the FO event handler that must be wrapped by this instance
     */
    public FO2StructureTreeConverter(StructureTreeEventHandler structureTreeEventHandler,
            FOEventHandler delegate) {
        super(delegate);
        this.structureTreeEventTrigger = new StructureTreeEventTrigger(structureTreeEventHandler);
        this.converter = structureTreeEventTrigger;
    }

    @Override
    public void startDocument() throws SAXException {
        converter.startDocument();
        super.startDocument();
    }

    @Override
    public void endDocument() throws SAXException {
        converter.endDocument();
        super.endDocument();
    }

    @Override
    public void startRoot(Root root) {
       converter.startRoot(root);
       super.startRoot(root);
    }

    @Override
    public void endRoot(Root root) {
        converter.endRoot(root);
        super.endRoot(root);
    }

    @Override
    public void startPageSequence(PageSequence pageSeq) {
        converter.startPageSequence(pageSeq);
        super.startPageSequence(pageSeq);
    }

    @Override
    public void endPageSequence(PageSequence pageSeq) {
        converter.endPageSequence(pageSeq);
        super.endPageSequence(pageSeq);
    }

    @Override
    public void startPageNumber(final PageNumber pagenum) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startPageNumber(pagenum);
            }
        }, true);
        super.startPageNumber(pagenum);
    }

    @Override
    public void endPageNumber(final PageNumber pagenum) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endPageNumber(pagenum);
            }
        });
        super.endPageNumber(pagenum);
    }

    @Override
    public void startPageNumberCitation(final PageNumberCitation pageCite) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startPageNumberCitation(pageCite);
            }
        }, true);
        super.startPageNumberCitation(pageCite);
    }

    @Override
    public void endPageNumberCitation(final PageNumberCitation pageCite) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endPageNumberCitation(pageCite);
            }
        });
        super.endPageNumberCitation(pageCite);
    }

    @Override
    public void startPageNumberCitationLast(final PageNumberCitationLast pageLast) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startPageNumberCitationLast(pageLast);
            }
        }, true);
        super.startPageNumberCitationLast(pageLast);
    }

    @Override
    public void endPageNumberCitationLast(final PageNumberCitationLast pageLast) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endPageNumberCitationLast(pageLast);
            }
        });
        super.endPageNumberCitationLast(pageLast);
    }

    @Override
    public void startStatic(final StaticContent staticContent) {
        handleStartArtifact(staticContent);
        startContent(new Event(this) {
            public void run() {
                eventHandler.startStatic(staticContent);
            }
        }, true);
        super.startStatic(staticContent);
    }

    @Override
    public void endStatic(final StaticContent staticContent) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endStatic(staticContent);
            }
        });
        handleEndArtifact(staticContent);
        super.endStatic(staticContent);
    }

    @Override
    public void startFlow(Flow fl) {
        converter.startFlow(fl);
        super.startFlow(fl);
    }

    @Override
    public void endFlow(Flow fl) {
        converter.endFlow(fl);
        super.endFlow(fl);
    }

    @Override
    public void startBlock(final Block bl) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startBlock(bl);
            }
        }, false);
        super.startBlock(bl);
    }

    @Override
    public void endBlock(final Block bl) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endBlock(bl);
            }
        });
        super.endBlock(bl);
    }

    @Override
    public void startBlockContainer(final BlockContainer blc) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startBlockContainer(blc);
            }
        }, false);
        super.startBlockContainer(blc);
    }

    @Override
    public void endBlockContainer(final BlockContainer blc) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endBlockContainer(blc);
            }
        });
        super.endBlockContainer(blc);
    }

    @Override
    public void startInline(final Inline inl) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startInline(inl);
            }
        }, true);
        super.startInline(inl);
    }

    @Override
    public void endInline(final Inline inl) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endInline(inl);
            }
        });
        super.endInline(inl);
    }

    @Override
    public void startTable(final Table tbl) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startTable(tbl);
            }
        }, true);
        super.startTable(tbl);
    }

    @Override
    public void endTable(final Table tbl) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endTable(tbl);
            }
        });
        super.endTable(tbl);
    }

    @Override
    public void startColumn(final TableColumn tc) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startColumn(tc);
            }
        }, true);
        super.startColumn(tc);
    }

    @Override
    public void endColumn(final TableColumn tc) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endColumn(tc);
            }
        });
        super.endColumn(tc);
    }

    @Override
    public void startHeader(final TableHeader header) {
        handleStartArtifact(header);
        startContent(new Event(this) {
            public void run() {
                eventHandler.startHeader(header);
            }
        }, false);
        super.startHeader(header);
    }

    @Override
    public void endHeader(final TableHeader header) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endHeader(header);
            }
        });
        handleEndArtifact(header);
        super.endHeader(header);
    }

    @Override
    public void startFooter(final TableFooter footer) {
        handleStartArtifact(footer);
        startContent(new Event(this) {
            public void run() {
                eventHandler.startFooter(footer);
            }
        }, false);
        super.startFooter(footer);
    }

    @Override
    public void endFooter(final TableFooter footer) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endFooter(footer);
            }
        });
        handleEndArtifact(footer);
        super.endFooter(footer);
    }

    @Override
    public void startBody(final TableBody body) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startBody(body);
            }
        }, true);
        super.startBody(body);
    }

    @Override
    public void endBody(final TableBody body) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endBody(body);
            }
        });
        super.endBody(body);
    }

    @Override
    public void startRow(final TableRow tr) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startRow(tr);
            }
        }, false);
        super.startRow(tr);
    }

    @Override
    public void endRow(final TableRow tr) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endRow(tr);
            }
        });
        super.endRow(tr);
    }

    @Override
    public void startCell(final TableCell tc) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startCell(tc);
            }
        }, false);
        super.startCell(tc);
    }

    @Override
    public void endCell(final TableCell tc) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endCell(tc);
            }
        });
        super.endCell(tc);
    }

    @Override
    public void startList(final ListBlock lb) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startList(lb);
            }
        }, true);
        super.startList(lb);
    }

    @Override
    public void endList(final ListBlock lb) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endList(lb);
            }
        });
        super.endList(lb);
    }

    @Override
    public void startListItem(final ListItem li) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startListItem(li);
            }
        }, true);
        super.startListItem(li);
    }

    @Override
    public void endListItem(final ListItem li) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endListItem(li);
            }
        });
        super.endListItem(li);
    }

    @Override
    public void startListLabel(final ListItemLabel listItemLabel) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startListLabel(listItemLabel);
            }
        }, true);
        super.startListLabel(listItemLabel);
    }

    @Override
    public void endListLabel(final ListItemLabel listItemLabel) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endListLabel(listItemLabel);
            }
        });
        super.endListLabel(listItemLabel);
    }

    @Override
    public void startListBody(final ListItemBody listItemBody) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startListBody(listItemBody);
            }
        }, true);
        super.startListBody(listItemBody);
    }

    @Override
    public void endListBody(final ListItemBody listItemBody) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endListBody(listItemBody);
            }
        });
        super.endListBody(listItemBody);
    }

    @Override
    public void startMarkup() {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startMarkup();
            }
        }, true);
        super.startMarkup();
    }

    @Override
    public void endMarkup() {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endMarkup();
            }
        });
        super.endMarkup();
    }

    @Override
    public void startLink(final BasicLink basicLink) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startLink(basicLink);
            }
        }, true);
        super.startLink(basicLink);
    }

    @Override
    public void endLink(final BasicLink basicLink) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endLink(basicLink);
            }
        });
        super.endLink(basicLink);
    }

    @Override
    public void image(final ExternalGraphic eg) {
        content(new Event(this) {
            public void run() {
                eventHandler.image(eg);
            }
        }, true);
        super.image(eg);
    }

    @Override
    public void pageRef() {
        content(new Event(this) {
            public void run() {
                eventHandler.pageRef();
            }
        }, true);
        super.pageRef();
    }

    @Override
    public void startInstreamForeignObject(final InstreamForeignObject ifo) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startInstreamForeignObject(ifo);
            }
        }, true);
        super.startInstreamForeignObject(ifo);
    }

    @Override
    public void endInstreamForeignObject(final InstreamForeignObject ifo) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endInstreamForeignObject(ifo);
            }
        });
        super.endInstreamForeignObject(ifo);
    }

    @Override
    public void startFootnote(final Footnote footnote) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startFootnote(footnote);
            }
        }, true);
        super.startFootnote(footnote);
    }

    @Override
    public void endFootnote(final Footnote footnote) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endFootnote(footnote);
            }
        });
        super.endFootnote(footnote);
    }

    @Override
    public void startFootnoteBody(final FootnoteBody body) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startFootnoteBody(body);
            }
        }, true);
        super.startFootnoteBody(body);
    }

    @Override
    public void endFootnoteBody(final FootnoteBody body) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endFootnoteBody(body);
            }
        });
        super.endFootnoteBody(body);
    }

    @Override
    public void startLeader(final Leader l) {
        converters.push(converter);
        converter = eventSwallower;
        startContent(new Event(this) {
            public void run() {
                eventHandler.startLeader(l);
            }
        }, false);
        super.startLeader(l);
    }

    @Override
    public void endLeader(final Leader l) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endLeader(l);
            }
        });
        converter = converters.pop();
        super.endLeader(l);
    }

    @Override
    public void startWrapper(final Wrapper wrapper) {
        handleStartArtifact(wrapper);
        startContent(new Event(this) {
            public void run() {
                eventHandler.startWrapper(wrapper);
            }
        }, true);
        super.startWrapper(wrapper);
    }

    @Override
    public void endWrapper(final Wrapper wrapper) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endWrapper(wrapper);
            }
        });
        handleEndArtifact(wrapper);
        super.endWrapper(wrapper);
    }

    @Override
    public void startRetrieveMarker(final RetrieveMarker retrieveMarker) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startRetrieveMarker(retrieveMarker);
            }
        }, true);
        saveState(retrieveMarker);
        super.startRetrieveMarker(retrieveMarker);
    }

    private void saveState(AbstractRetrieveMarker retrieveMarker) {
        states.put(retrieveMarker, new State(this));
    }

    @Override
    public void endRetrieveMarker(final RetrieveMarker retrieveMarker) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endRetrieveMarker(retrieveMarker);
            }
        });
        super.endRetrieveMarker(retrieveMarker);
    }

    @Override
    public void restoreState(final RetrieveMarker retrieveMarker) {
        restoreRetrieveMarkerState(retrieveMarker);
        content(new Event(this) {
            public void run() {
                eventHandler.restoreState(retrieveMarker);
            }
        }, true);
        super.restoreState(retrieveMarker);
    }

    @SuppressWarnings("unchecked")
    private void restoreRetrieveMarkerState(AbstractRetrieveMarker retrieveMarker) {
        State state = states.get(retrieveMarker);
        this.converter = state.converter;
        this.converters = (Stack<FOEventHandler>) state.converters.clone();
    }

    @Override
    public void startRetrieveTableMarker(final RetrieveTableMarker retrieveTableMarker) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startRetrieveTableMarker(retrieveTableMarker);
            }
        }, true);
        saveState(retrieveTableMarker);
        super.startRetrieveTableMarker(retrieveTableMarker);
    }

    @Override
    public void endRetrieveTableMarker(final RetrieveTableMarker retrieveTableMarker) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endRetrieveTableMarker(retrieveTableMarker);
            }
        });
        super.endRetrieveTableMarker(retrieveTableMarker);
    }

    @Override
    public void restoreState(final RetrieveTableMarker retrieveTableMarker) {
        restoreRetrieveMarkerState(retrieveTableMarker);
        currentNode.add(new Event(this) {
            public void run() {
                eventHandler.restoreState(retrieveTableMarker);
            }
        });
        super.restoreState(retrieveTableMarker);
    }

    @Override
    public void character(final Character c) {
        content(new Event(this) {
            public void run() {
                eventHandler.character(c);
            }
        }, true);
        super.character(c);
    }

    @Override
    public void characters(final FOText foText) {
        content(new Event(this) {
            public void run() {
                eventHandler.characters(foText);
            }
        }, foText.length() > 0);
        super.characters(foText);
    }

    @Override
    public void startExternalDocument(final ExternalDocument document) {
        startContent(new Event(this) {
            public void run() {
                eventHandler.startExternalDocument(document);
            }
        }, true);
        super.startExternalDocument(document);
    }

    @Override
    public void endExternalDocument(final ExternalDocument document) {
        endContent(new Event(this) {
            public void run() {
                eventHandler.endExternalDocument(document);
            }
        });
        super.endExternalDocument(document);
    }

    private void handleStartArtifact(CommonAccessibilityHolder fobj) {
        if (isArtifact(fobj)) {
            converters.push(converter);
            converter = eventSwallower;
        }
    }

    private void handleEndArtifact(CommonAccessibilityHolder fobj) {
        if (isArtifact(fobj)) {
            converter = converters.pop();
        }
    }

    private boolean isArtifact(CommonAccessibilityHolder fobj) {
        CommonAccessibility accessibility = fobj.getCommonAccessibility();
        return Accessibility.ROLE_ARTIFACT.equalsIgnoreCase(accessibility.getRole());
    }

}
