package com.atlassian.webdriver.bitbucket.element;

import com.atlassian.bitbucket.util.MoreStreams;
import com.atlassian.pageobjects.PageBinder;
import com.atlassian.pageobjects.binder.WaitUntil;
import com.atlassian.pageobjects.elements.PageElement;
import com.atlassian.pageobjects.elements.PageElementFinder;
import com.atlassian.pageobjects.elements.query.Poller;
import com.atlassian.pageobjects.elements.timeout.TimeoutType;
import com.atlassian.webdriver.bitbucket.page.BranchListPage;
import com.atlassian.webdriver.bitbucket.page.PullRequestCreatePage;
import com.atlassian.webdriver.bitbucket.page.admin.repo.BranchPermissionsPage;
import com.atlassian.webdriver.bitbucket.page.compare.CommitsComparePage;
import com.atlassian.webdriver.bitbucket.util.DateUtils;
import com.atlassian.webdriver.bitbucket.util.ElementUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import org.openqa.selenium.By;

import java.time.Instant;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class BranchList {

    private final BranchListPage parent;
    private final PageElement tableElement;
    private final PageElement filterElement;
    private Long lastUpdated;

    @Inject
    private PageBinder pageBinder;

    @Inject
    private PageElementFinder elementFinder;

    public BranchList(BranchListPage parent, PageElement tableElement, PageElement filterElement, Long lastUpdated) {
        this.parent = parent;
        this.tableElement = tableElement;
        this.filterElement = filterElement;
        this.lastUpdated = lastUpdated;
    }

    @WaitUntil
    public void ensureLoaded() {
        ElementUtils.waitUntilUpdated(tableElement, lastUpdated);
        lastUpdated = ElementUtils.getLastUpdated(tableElement);
    }

    public List<BranchListEntry> getRows() {
        return ImmutableList.copyOf(rowIterator());
    }

    public BranchListEntry getRow(final String name) {
        return MoreStreams.streamIterable(rowIterator())
                .filter(input -> input.getName().equals(name))
                .findFirst()
                .orElse(null);
    }

    public BranchListEntry getRowOrFail(String name) {
        BranchListEntry entry = getRow(name);
        if (entry == null) {
            throw new RuntimeException(String.format("Unable to find branch '%s'.", name));
        } else {
            return entry;
        }
    }

    public BranchListEntry getFocusedRow() {
        PageElement focusedElement = tableElement.find(By.cssSelector("tbody > tr.focused"));
        Poller.waitUntilTrue(focusedElement.timed().isVisible());
        return pageBinder.bind(BranchListEntry.class, BranchList.this, focusedElement);
    }

    public BranchListEntry getDefaultBranchRow() {
        return getRowWithLozenge("default branch");
    }

    public BranchListEntry getBaseBranchRow() {
        return getRowWithLozenge("base branch");
    }

    public List<String> getColumnNames() {
        final List<PageElement> headers = tableElement.findAll(By.tagName("th"));
        return Lists.transform(headers, PageElement::getText);
    }

    public BranchList loadNextPage() {
        PageElement messageElement = getMessageElement();
        if (!messageElement.isPresent()) {
            ElementUtils.scrollDocumentAndWait(tableElement);
        }
        return this;
    }

    public boolean isFilterFocused() {
        // Firefox window needs to be focused!!!
        return (Boolean) filterElement.javascript().execute("return jQuery(arguments[0]).is(':focus')");
    }

    public BranchList filter(final String filterText) {
        ElementUtils.waitUntilUpdated(tableElement, () -> {
            filterElement.type(filterText);
        });

        return this;
    }

    public boolean isBuildStatusColumnVisible() {
        return tableElement.find(By.id("build-status-branch-list-col")).isVisible();
    }

    private BranchListEntry getRowWithLozenge(String lozenge) {
        Poller.waitUntilFalse(parent.isLoadingTimed());
        for (BranchListEntry row : getRows()) {
            if (row.getLozenges().contains(lozenge)) {
                return row;
            }
        }

        return null;
    }

    private PageElement getMessageElement() {
        return elementFinder.find(By.className("paged-table-message"));
    }

    public String getMessageText() {
        PageElement message = getMessageElement();
        return message.isVisible() ? message.getText() : null;
    }

    public Long getLastUpdated() {
        return lastUpdated;
    }

    private Iterable<BranchListEntry> rowIterator() {
        return Iterables.transform(tableElement.findAll(By.cssSelector("tbody > tr")),
                element -> pageBinder.bind(BranchListEntry.class, BranchList.this, element));
    }

    public static class BranchListEntry {

        private final BranchList parent;
        private final PageElement element;

        public BranchListEntry(BranchList parent, PageElement element) {
            this.parent = parent;
            this.element = element;
        }

        public String getName() {
            return getBranchLink().getText();
        }

        public List<String> getLozenges() {
            return Lists.newArrayList(Lists.transform(element.findAll(By.className("aui-lozenge")),
                    losenge -> losenge.getText().toLowerCase()));
        }

        public Instant getUpdated() {
            PageElement timeElement = getColumn("last-updated-column").find(By.tagName("time"));
            return timeElement.isPresent() ?
                    DateUtils.parseServerTimestamp(timeElement.getAttribute("datetime")).toInstant() :
                    null;
        }

        public Integer getAheadCount() {
            return getCommitCount("ahead");
        }

        public Integer getBehindCount() {
            return getCommitCount("behind");
        }

        public boolean hasBuildStatus() {
            PageElement cell = getColumn("build-status-branch-list-col");
            return cell.isPresent() && cell.find(By.className("build-icon")).isPresent();
        }

        public BuildStatusDialog clickBuildStatus() {
            getColumn("build-status-branch-list-col").find(By.className("build-icon")).click();
            return parent.pageBinder.bind(BuildStatusDialog.class, By.id("build-status-dialog"), TimeoutType.DIALOG_LOAD);
        }

        public String getAheadBehindToolTip() {
            PageElement graphElement = getColumn("ahead-behind-column").find(By.className("ahead-behind-graph"));
            if (!graphElement.isPresent()) {
                graphElement = getColumn("ahead-behind-column");
            }
            return getTitle(graphElement);
        }

        public String getPullRequestStatus() {
            PageElement lozenge = getColumn("pull-requests-column").find(By.className("aui-lozenge"));
            if (lozenge.isPresent()) {
                return lozenge.getText();
            }
            return null;
        }

        public boolean hasPullRequestColumn() {
            PageElement pageElement = element.find(By.cssSelector("td[headers=pull-requests-column]"));
            return pageElement.isPresent();
        }

        public String getPullRequestLinkTooltip() {
            PageElement link = getColumn("pull-requests-column").find(By.className("pull-request-list-trigger"));
            if (link.isPresent()) {
                return getTitle(link);
            }
            return null;
        }

        public String getPullRequestLinkHref() {
            PageElement link = getColumn("pull-requests-column").find(By.tagName("a"));
            if (link.isPresent()) {
                return link.getAttribute("href");
            }
            return null;
        }

        public PullRequestListDialog clickPullRequestLink() {
            getColumn("pull-requests-column").find(By.className("pull-request-list-trigger")).click();
            return parent.pageBinder.bind(PullRequestListDialog.class, By.id("pull-request-list-dialog"), TimeoutType.DIALOG_LOAD);
        }

        private Integer getCommitCount(String type) {
            PageElement countElement = getColumn("ahead-behind-column").find(By.cssSelector("." + type + "-graph .count"));
            if (countElement.isPresent()) {
                String count = countElement.getText().trim();

                return count.isEmpty() ? 0 : Integer.parseInt(count);
            }

            return null;
        }

        public CommitsComparePage click() {
            getBranchLink().click();
            return parent.pageBinder.bind(CommitsComparePage.class, parent.parent.getProjectKey(), parent.parent.getSlug());
        }

        public BranchPermissionsPage clickEditBranchPermissions() {
            clickAction("edit-permissions");
            return parent.pageBinder.bind(BranchPermissionsPage.class, parent.parent.getProjectKey(), parent.parent.getSlug());
        }

        public PullRequestCreatePage clickCreatePullRequest() {
            clickAction("create-pull-request");
            return parent.pageBinder.bind(PullRequestCreatePage.class, parent.parent.getProjectKey(), parent.parent.getSlug());
        }

        public BranchDeleteDialog clickDeleteBranch() {
            clickAction("delete-branch");
            return parent.pageBinder.bind(
                    BranchDeleteDialog.class, By.id("delete-branch-dialog"), TimeoutType.DIALOG_LOAD,
                    BranchListPage.class, new Object[]{parent.parent.getProjectKey(), parent.parent.getSlug(), parent.lastUpdated});
        }

        public List<String> getActionClasses() {
            PageElement menu = parent.elementFinder.find(By.id("branch-list-actions-menu-refs/heads/" + getName()));
            return Lists.newArrayList(Lists.transform(menu.findAll(By.tagName("a")), input -> {
                String attribute = input.getAttribute("class");
                //remove the state from the action link classes
                if (attribute != null) {
                    attribute = attribute.replace("active", "");
                    attribute = attribute.trim();
                }
                return attribute;
            }));
        }

        public boolean isAheadBehindNotAvailable() {
            return getColumn("ahead-behind-column").getText().contains("Not available");
        }

        private void clickAction(String className) {
            this.element.find(By.className("branch-list-action-trigger")).click();
            PageElement menu = parent.elementFinder.find(By.id("branch-list-actions-menu-refs/heads/" + getName()));
            Poller.waitUntilTrue(menu.timed().isVisible());
            menu.find(By.className(className)).click();
        }

        private PageElement getBranchLink() {
            return getColumn("branch-name-column").find(By.tagName("a"));
        }

        private PageElement getColumn(String name) {
            PageElement pageElement = element.find(By.cssSelector("td[headers=" + name + "]"));
            Poller.waitUntilTrue("Column " + name + " should be present", pageElement.timed().isPresent());
            return pageElement;
        }

        private String getTitle(PageElement graphElement) {
            String titleAttribute = graphElement.getAttribute("title");
            return titleAttribute == null || titleAttribute.isEmpty() ?
                    graphElement.getAttribute("original-title") : titleAttribute;
        }
    }

    public static class BranchDeleteDialog extends DeleteConfirmationDialog<BranchListPage> {

        private static final String NOT_MERGED_ID = "delete-branch-not-merged-warning";
        private static final Pattern NOT_MERGED_MESSAGE = Pattern.compile("branch has (\\d+) commits?.*not on (.+)$");

        public BranchDeleteDialog(By locator, TimeoutType timeoutType, Class<BranchListPage> page, Object... args) {
            super(locator, timeoutType, page, args);
        }

        public boolean hasNotMergedWarning() {
            return findNotMerged() != null;
        }

        public long getNotMergedCommitCount() {
            return Long.parseLong(getNotMergedMatcher().group(1));
        }

        public String getNotMergedBranch() {
            return getNotMergedMatcher().group(2);
        }

        private Matcher getNotMergedMatcher() {
            Matcher matcher = NOT_MERGED_MESSAGE.matcher(findNotMergedOrFail().getText());
            if (matcher.find()) {
                return matcher;
            } else {
                throw new RuntimeException("Unable to parse message in delete dialog.");
            }
        }

        private PageElement findNotMerged() {
            final PageElement element = dialogPanelBody.find(By.id(NOT_MERGED_ID));
            if (element.isPresent()) {
                return element;
            } else {
                return null;
            }
        }

        private PageElement findNotMergedOrFail() {
            PageElement notMergedElement = findNotMerged();
            if (notMergedElement == null) {
                throw new RuntimeException("Dialog does not have not merged message.");
            } else {
                return notMergedElement;
            }
        }
    }
}
