/*
 * 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 org.apache.maven.doxia.site.inheritance;

import javax.inject.Named;
import javax.inject.Singleton;

import java.util.ArrayList;
import java.util.List;

import org.apache.maven.doxia.site.Body;
import org.apache.maven.doxia.site.LinkItem;
import org.apache.maven.doxia.site.Logo;
import org.apache.maven.doxia.site.Menu;
import org.apache.maven.doxia.site.MenuItem;
import org.apache.maven.doxia.site.SiteModel;
import org.codehaus.plexus.util.xml.Xpp3Dom;

/**
 * Manage inheritance of the site model.
 *
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
 */
@Singleton
@Named
public class DefaultSiteModelInheritanceAssembler implements SiteModelInheritanceAssembler {
    /** {@inheritDoc} */
    public void assembleModelInheritance(
            String name, SiteModel child, SiteModel parent, String childBaseUrl, String parentBaseUrl) {
        if (parent == null || !child.isMergeParent()) {
            return;
        }

        child.setCombineSelf(parent.getCombineSelf());

        URLRebaser urlContainer = new URLRebaser(parentBaseUrl, childBaseUrl);

        if (child.getBannerLeft() == null && parent.getBannerLeft() != null) {
            child.setBannerLeft(parent.getBannerLeft().clone());
            rebaseLinkItemPaths(child.getBannerLeft(), urlContainer);
        }

        if (child.getBannerRight() == null && parent.getBannerRight() != null) {
            child.setBannerRight(parent.getBannerRight().clone());
            rebaseLinkItemPaths(child.getBannerRight(), urlContainer);
        }

        if (child.isDefaultPublishDate() && parent.getPublishDate() != null) {
            child.setPublishDate(parent.getPublishDate().clone());
        }

        if (child.isDefaultVersion() && parent.getVersion() != null) {
            child.setVersion(parent.getVersion().clone());
        }

        if (child.getEdit() == null && parent.getEdit() != null) {
            child.setEdit(parent.getEdit());
        }

        if (child.getSkin() == null && parent.getSkin() != null) {
            child.setSkin(parent.getSkin().clone());
        }

        child.setPoweredBy(mergePoweredByLists(child.getPoweredBy(), parent.getPoweredBy(), urlContainer));

        if (parent.getLastModified() > child.getLastModified()) {
            child.setLastModified(parent.getLastModified());
        }

        assembleBodyInheritance(name, child, parent, urlContainer);

        assembleCustomInheritance(child, parent);
    }

    /** {@inheritDoc} */
    public void resolvePaths(final SiteModel siteModel, final String baseUrl) {
        if (baseUrl == null) {
            return;
        }

        if (siteModel.getBannerLeft() != null) {
            relativizeLinkItemPaths(siteModel.getBannerLeft(), baseUrl);
        }

        if (siteModel.getBannerRight() != null) {
            relativizeLinkItemPaths(siteModel.getBannerRight(), baseUrl);
        }

        for (Logo logo : siteModel.getPoweredBy()) {
            relativizeLinkItemPaths(logo, baseUrl);
        }

        if (siteModel.getBody() != null) {
            for (LinkItem linkItem : siteModel.getBody().getLinks()) {
                relativizeLinkItemPaths(linkItem, baseUrl);
            }

            for (LinkItem linkItem : siteModel.getBody().getBreadcrumbs()) {
                relativizeLinkItemPaths(linkItem, baseUrl);
            }

            for (Menu menu : siteModel.getBody().getMenus()) {
                relativizeMenuPaths(menu.getItems(), baseUrl);
                if (menu.getImage() != null) {
                    menu.getImage().setSrc(relativizeLink(menu.getImage().getSrc(), baseUrl));
                }
            }
        }
    }

    private void assembleCustomInheritance(final SiteModel child, final SiteModel parent) {
        if (child.getCustom() == null) {
            child.setCustom(parent.getCustom());
        } else {
            child.setCustom(Xpp3Dom.mergeXpp3Dom((Xpp3Dom) child.getCustom(), (Xpp3Dom) parent.getCustom()));
        }
    }

    private void assembleBodyInheritance(
            final String name, final SiteModel child, final SiteModel parent, final URLRebaser urlContainer) {
        Body cBody = child.getBody();
        Body pBody = parent.getBody();

        if (cBody != null || pBody != null) {
            if (cBody == null) {
                cBody = new Body();
                child.setBody(cBody);
            }

            if (pBody == null) {
                pBody = new Body();
            }

            if (cBody.getHead() == null && pBody.getHead() != null) {
                cBody.setHead(pBody.getHead());
            }

            cBody.setLinks(mergeLinkItemLists(cBody.getLinks(), pBody.getLinks(), urlContainer, false));

            if (cBody.getBreadcrumbs().isEmpty() && !pBody.getBreadcrumbs().isEmpty()) {
                LinkItem breadcrumb = new LinkItem();
                breadcrumb.setName(name);
                breadcrumb.setHref("index.html");
                cBody.getBreadcrumbs().add(breadcrumb);
            }
            cBody.setBreadcrumbs(
                    mergeLinkItemLists(cBody.getBreadcrumbs(), pBody.getBreadcrumbs(), urlContainer, true));

            cBody.setMenus(mergeMenus(cBody.getMenus(), pBody.getMenus(), urlContainer));

            if (cBody.getFooter() == null && pBody.getFooter() != null) {
                cBody.setFooter(pBody.getFooter());
            }
        }
    }

    private List<Menu> mergeMenus(
            final List<Menu> childMenus, final List<Menu> parentMenus, final URLRebaser urlContainer) {
        List<Menu> menus = new ArrayList<>(childMenus.size() + parentMenus.size());

        for (Menu menu : childMenus) {
            menus.add(menu);
        }

        int topCounter = 0;
        for (Menu menu : parentMenus) {
            if ("top".equals(menu.getInherit())) {
                final Menu clone = menu.clone();

                rebaseMenuPaths(clone.getItems(), urlContainer);
                if (clone.getImage() != null) {
                    clone.getImage()
                            .setSrc(urlContainer.rebaseLink(clone.getImage().getSrc()));
                }

                menus.add(topCounter, clone);
                topCounter++;
            } else if ("bottom".equals(menu.getInherit())) {
                final Menu clone = menu.clone();

                rebaseMenuPaths(clone.getItems(), urlContainer);
                if (clone.getImage() != null) {
                    clone.getImage()
                            .setSrc(urlContainer.rebaseLink(clone.getImage().getSrc()));
                }

                menus.add(clone);
            }
        }

        return menus;
    }

    private void relativizeMenuPaths(final List<MenuItem> items, final String baseUrl) {
        for (MenuItem item : items) {
            relativizeLinkItemPaths(item, baseUrl);
            relativizeMenuPaths(item.getItems(), baseUrl);
        }
    }

    private void rebaseMenuPaths(final List<MenuItem> items, final URLRebaser urlContainer) {
        for (MenuItem item : items) {
            rebaseLinkItemPaths(item, urlContainer);
            rebaseMenuPaths(item.getItems(), urlContainer);
        }
    }

    private void relativizeLinkItemPaths(final LinkItem item, final String baseUrl) {
        item.setHref(relativizeLink(item.getHref(), baseUrl));
        if (item.getImage() != null) {
            item.getImage().setSrc(relativizeLink(item.getImage().getSrc(), baseUrl));
        }
    }

    private void rebaseLinkItemPaths(final LinkItem item, final URLRebaser urlContainer) {
        item.setHref(urlContainer.rebaseLink(item.getHref()));
        if (item.getImage() != null) {
            item.getImage().setSrc(urlContainer.rebaseLink(item.getImage().getSrc()));
        }
    }

    private List<LinkItem> mergeLinkItemLists(
            final List<LinkItem> childList,
            final List<LinkItem> parentList,
            final URLRebaser urlContainer,
            boolean cutParentAfterDuplicate) {
        List<LinkItem> items = new ArrayList<>(childList.size() + parentList.size());

        for (LinkItem item : parentList) {
            if (!items.contains(item) && !childList.contains(item)) {
                final LinkItem clone = item.clone();

                rebaseLinkItemPaths(clone, urlContainer);

                items.add(clone);
            } else if (cutParentAfterDuplicate) {
                // if a parent item is found in child, ignore next items (case for breadcrumbs)
                // merge ( "B > E", "A > B > C > D" ) -> "A > B > E" (notice missing "C > D")
                // see https://issues.apache.org/jira/browse/DOXIASITETOOLS-62
                break;
            }
        }

        for (LinkItem item : childList) {
            if (!items.contains(item)) {
                items.add(item);
            }
        }

        return items;
    }

    private List<Logo> mergePoweredByLists(
            final List<Logo> childList, final List<Logo> parentList, final URLRebaser urlContainer) {
        List<Logo> logos = new ArrayList<>(childList.size() + parentList.size());

        for (Logo logo : parentList) {
            if (!logos.contains(logo)) {
                final Logo clone = logo.clone();

                rebaseLinkItemPaths(clone, urlContainer);

                logos.add(clone);
            }
        }

        for (Logo logo : childList) {
            if (!logos.contains(logo)) {
                logos.add(logo);
            }
        }

        return logos;
    }

    // relativize only affects absolute links, if the link has the same scheme, host and port
    // as the base, it is made into a relative link as viewed from the base
    private String relativizeLink(final String link, final String baseUri) {
        if (link == null || baseUri == null) {
            return link;
        }

        // this shouldn't be necessary, just to swallow mal-formed hrefs
        try {
            final URIPathDescriptor path = new URIPathDescriptor(baseUri, link);

            return path.relativizeLink().toString();
        } catch (IllegalArgumentException e) {
            return link;
        }
    }

    /**
     * URL rebaser: based on an old and a new path, can rebase a link based on old path to a value based on the new
     * path.
     */
    private static class URLRebaser {

        private final String oldPath;

        private final String newPath;

        /**
         * Construct a URL rebaser.
         *
         * @param oldPath the old path.
         * @param newPath the new path.
         */
        URLRebaser(final String oldPath, final String newPath) {
            this.oldPath = oldPath;
            this.newPath = newPath;
        }

        /**
         * Get the new path.
         *
         * @return the new path.
         */
        public String getNewPath() {
            return this.newPath;
        }

        /**
         * Get the old path.
         *
         * @return the old path.
         */
        public String getOldPath() {
            return this.oldPath;
        }

        /**
         * Rebase only affects relative links, a relative link wrt an old base gets translated,
         * so it points to the same location as viewed from a new base
         */
        public String rebaseLink(final String link) {
            if (link == null || getOldPath() == null) {
                return link;
            }

            if (link.contains("${project.")) {
                throw new IllegalArgumentException("site.xml late interpolation ${project.*} expression found"
                        + " in link: '" + link + "'. Use early interpolation ${this.*}");
            }

            final URIPathDescriptor oldPath = new URIPathDescriptor(getOldPath(), link);

            return oldPath.rebaseLink(getNewPath()).toString();
        }
    }
}
