/****************************************************************
 * 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.james.jmap.model;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

@JsonDeserialize(builder = FilterCondition.Builder.class)
public class FilterCondition implements Filter {

    public static Builder builder() {
        return new Builder();
    }

    @JsonPOJOBuilder(withPrefix = "")
    public static class Builder {

        private Optional<List<String>> inMailboxes;
        private Optional<List<String>> notInMailboxes;
        private ZonedDateTime before;
        private ZonedDateTime after;
        private Number minSize;
        private Number maxSize;
        private Boolean isFlagged;
        private Boolean isUnread;
        private Boolean isAnswered;
        private Boolean isForwarded;
        private Boolean isDraft;
        private Boolean hasAttachment;
        private String text;
        private String from;
        private String to;
        private String cc;
        private String bcc;
        private String subject;
        private String body;
        private String attachments;
        private Header header;
        private Optional<String> hasKeyword;
        private Optional<String> notKeyword;
        private Optional<String> attachmentFileName;

        private Builder() {
            inMailboxes = Optional.empty();
            notInMailboxes = Optional.empty();
            hasKeyword = Optional.empty();
            notKeyword = Optional.empty();
            attachmentFileName = Optional.empty();
        }

        public Builder inMailboxes(String... inMailboxes) {
            this.inMailboxes = Optional.of(ImmutableList.copyOf(inMailboxes));
            return this;
        }

        @JsonDeserialize
        public Builder inMailboxes(Optional<List<String>> inMailboxes) {
            this.inMailboxes = inMailboxes.map(ImmutableList::copyOf);
            return this;
        }

        public Builder notInMailboxes(String... notInMailboxes) {
            this.notInMailboxes = Optional.of(ImmutableList.copyOf(notInMailboxes));
            return this;
        }

        @JsonDeserialize
        public Builder notInMailboxes(Optional<List<String>> notInMailboxes) {
            this.notInMailboxes = notInMailboxes.map(ImmutableList::copyOf);
            return this;
        }

        @JsonDeserialize
        public Builder hasKeyword(Optional<String> hasKeyword) {
            this.hasKeyword = hasKeyword;
            return this;
        }

        @JsonDeserialize
        public Builder notKeyword(Optional<String> notKeyword) {
            this.notKeyword = notKeyword;
            return this;
        }

        public Builder before(ZonedDateTime before) {
            this.before = before;
            return this;
        }

        public Builder after(ZonedDateTime after) {
            this.after = after;
            return this;
        }

        public Builder minSize(long minSize) {
            this.minSize = Number.DEFAULT_FACTORY.from(minSize)
                .orElseThrow(() -> new IllegalArgumentException(Number.VALIDATION_MESSAGE));
            return this;
        }

        public Builder maxSize(long maxSize) {
            this.maxSize = Number.DEFAULT_FACTORY.from(maxSize)
                .orElseThrow(() -> new IllegalArgumentException(Number.VALIDATION_MESSAGE));
            return this;
        }

        public Builder isFlagged(boolean isFlagged) {
            this.isFlagged = isFlagged;
            return this;
        }

        public Builder isUnread(boolean isUnread) {
            this.isUnread = isUnread;
            return this;
        }

        public Builder isAnswered(boolean isAnswered) {
            this.isAnswered = isAnswered;
            return this;
        }

        public Builder isDraft(boolean isDraft) {
            this.isDraft = isDraft;
            return this;
        }

        public Builder isForwarded(boolean isForwarded) {
            this.isForwarded = isForwarded;
            return this;
        }

        public Builder hasAttachment(boolean hasAttachment) {
            this.hasAttachment = hasAttachment;
            return this;
        }

        public Builder text(String text) {
            this.text = text;
            return this;
        }

        public Builder from(String from) {
            this.from = from;
            return this;
        }

        public Builder to(String to) {
            this.to = to;
            return this;
        }

        public Builder cc(String cc) {
            this.cc = cc;
            return this;
        }

        public Builder bcc(String bcc) {
            this.bcc = bcc;
            return this;
        }

        public Builder subject(String subject) {
            this.subject = subject;
            return this;
        }

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public Builder attachments(String attachments) {
            this.attachments = attachments;
            return this;
        }

        public Builder header(Header header) {
            this.header = header;
            return this;
        }

        public Builder attachmentFileName(Optional<String> attachmentFileName) {
            this.attachmentFileName = attachmentFileName;
            return this;
        }

        public FilterCondition build() {
            Preconditions.checkArgument(!hasKeyword.isPresent() || (Keyword.of(hasKeyword.get()) != null), "hasKeyword is not valid");
            Preconditions.checkArgument(!notKeyword.isPresent() || (Keyword.of(notKeyword.get()) != null), "notKeyword is not valid");
            return new FilterCondition(inMailboxes, notInMailboxes, Optional.ofNullable(before), Optional.ofNullable(after), Optional.ofNullable(minSize), Optional.ofNullable(maxSize),
                    Optional.ofNullable(isFlagged), Optional.ofNullable(isUnread), Optional.ofNullable(isAnswered), Optional.ofNullable(isDraft), Optional.ofNullable(isForwarded),
                    Optional.ofNullable(hasAttachment),
                    Optional.ofNullable(text), Optional.ofNullable(from), Optional.ofNullable(to), Optional.ofNullable(cc), Optional.ofNullable(bcc), Optional.ofNullable(subject),
                    Optional.ofNullable(body), Optional.ofNullable(attachments), Optional.ofNullable(header), hasKeyword, notKeyword, attachmentFileName);
        }
    }

    private final Optional<List<String>> inMailboxes;
    private final Optional<List<String>> notInMailboxes;
    private final Optional<ZonedDateTime> before;
    private final Optional<ZonedDateTime> after;
    private final Optional<Number> minSize;
    private final Optional<Number> maxSize;
    private final Optional<Boolean> isFlagged;
    private final Optional<Boolean> isUnread;
    private final Optional<Boolean> isAnswered;
    private final Optional<Boolean> isDraft;
    private final Optional<Boolean> isForwarded;
    private final Optional<Boolean> hasAttachment;
    private final Optional<String> text;
    private final Optional<String> from;
    private final Optional<String> to;
    private final Optional<String> cc;
    private final Optional<String> bcc;
    private final Optional<String> subject;
    private final Optional<String> body;
    private final Optional<String> attachments;
    private final Optional<Header> header;
    private final Optional<String> hasKeyword;
    private final Optional<String> notKeyword;
    private final Optional<String> attachmentFileName;

    @VisibleForTesting FilterCondition(Optional<List<String>> inMailboxes, Optional<List<String>> notInMailboxes, Optional<ZonedDateTime> before, Optional<ZonedDateTime> after, Optional<Number> minSize, Optional<Number> maxSize,
                                       Optional<Boolean> isFlagged, Optional<Boolean> isUnread, Optional<Boolean> isAnswered, Optional<Boolean> isDraft, Optional<Boolean> isForwarded,
                                       Optional<Boolean> hasAttachment,
                                       Optional<String> text, Optional<String> from, Optional<String> to, Optional<String> cc, Optional<String> bcc, Optional<String> subject,
                                       Optional<String> body, Optional<String> attachments, Optional<Header> header, Optional<String> hasKeyword, Optional<String> notKeyword, Optional<String> attachmentFileName) {

        this.inMailboxes = inMailboxes;
        this.notInMailboxes = notInMailboxes;
        this.before = before;
        this.after = after;
        this.minSize = minSize;
        this.maxSize = maxSize;
        this.isFlagged = isFlagged;
        this.isUnread = isUnread;
        this.isAnswered = isAnswered;
        this.isDraft = isDraft;
        this.isForwarded = isForwarded;
        this.hasAttachment = hasAttachment;
        this.text = text;
        this.from = from;
        this.to = to;
        this.cc = cc;
        this.bcc = bcc;
        this.subject = subject;
        this.body = body;
        this.attachments = attachments;
        this.header = header;
        this.hasKeyword = hasKeyword;
        this.notKeyword = notKeyword;
        this.attachmentFileName = attachmentFileName;
    }

    public Optional<List<String>> getInMailboxes() {
        return inMailboxes;
    }

    public Optional<List<String>> getNotInMailboxes() {
        return notInMailboxes;
    }

    public Optional<ZonedDateTime> getBefore() {
        return before;
    }

    public Optional<ZonedDateTime> getAfter() {
        return after;
    }

    public Optional<Number> getMinSize() {
        return minSize;
    }

    public Optional<Number> getMaxSize() {
        return maxSize;
    }

    public Optional<Boolean> getIsFlagged() {
        return isFlagged;
    }

    public Optional<Boolean> getIsUnread() {
        return isUnread;
    }

    public Optional<Boolean> getIsAnswered() {
        return isAnswered;
    }

    public Optional<Boolean> getIsDraft() {
        return isDraft;
    }

    public Optional<Boolean> getIsForwarded() {
        return isForwarded;
    }

    public Optional<Boolean> getHasAttachment() {
        return hasAttachment;
    }

    public Optional<String> getText() {
        return text;
    }

    public Optional<String> getFrom() {
        return from;
    }

    public Optional<String> getTo() {
        return to;
    }

    public Optional<String> getCc() {
        return cc;
    }

    public Optional<String> getBcc() {
        return bcc;
    }

    public Optional<String> getSubject() {
        return subject;
    }

    public Optional<String> getBody() {
        return body;
    }

    public Optional<String> getAttachments() {
        return attachments;
    }

    public Optional<Header> getHeader() {
        return header;
    }

    public Optional<String> getHasKeyword() {
        return hasKeyword;
    }

    public Optional<String> getNotKeyword() {
        return notKeyword;
    }

    public Optional<String> getAttachmentFileName() {
        return attachmentFileName;
    }

    @Override
    public final boolean equals(Object obj) {
        if (obj instanceof FilterCondition) {
            FilterCondition other = (FilterCondition) obj;
            return Objects.equals(this.inMailboxes, other.inMailboxes)
                && Objects.equals(this.notInMailboxes, other.notInMailboxes)
                && Objects.equals(this.before, other.before)
                && Objects.equals(this.after, other.after)
                && Objects.equals(this.minSize, other.minSize)
                && Objects.equals(this.maxSize, other.maxSize)
                && Objects.equals(this.isFlagged, other.isFlagged)
                && Objects.equals(this.isUnread, other.isUnread)
                && Objects.equals(this.isAnswered, other.isAnswered)
                && Objects.equals(this.isDraft, other.isDraft)
                && Objects.equals(this.isForwarded, other.isForwarded)
                && Objects.equals(this.hasAttachment, other.hasAttachment)
                && Objects.equals(this.text, other.text)
                && Objects.equals(this.from, other.from)
                && Objects.equals(this.to, other.to)
                && Objects.equals(this.cc, other.cc)
                && Objects.equals(this.bcc, other.bcc)
                && Objects.equals(this.subject, other.subject)
                && Objects.equals(this.body, other.body)
                && Objects.equals(this.attachments, other.attachments)
                && Objects.equals(this.header, other.header)
                && Objects.equals(this.hasKeyword, other.hasKeyword)
                && Objects.equals(this.notKeyword, other.notKeyword)
                && Objects.equals(this.attachmentFileName, other.attachmentFileName);
        }
        return false;
    }

    @Override
    public final int hashCode() {
        return Objects.hash(inMailboxes, notInMailboxes, before, after, minSize, maxSize, isFlagged, isUnread, isAnswered, isDraft, isForwarded, hasAttachment,
                text, from, to, cc, bcc, subject, body, attachments, header, hasKeyword, notKeyword, attachmentFileName);
    }

    @Override
    public String toString() {
        ToStringHelper helper = MoreObjects.toStringHelper(getClass());
        inMailboxes.ifPresent(x -> helper.add("inMailboxes", x));
        notInMailboxes.ifPresent(x -> helper.add("notInMailboxes", x));
        before.ifPresent(x -> helper.add("before", x));
        after.ifPresent(x -> helper.add("after", x));
        minSize.ifPresent(x -> helper.add("minSize", x));
        maxSize.ifPresent(x -> helper.add("maxSize", x));
        isFlagged.ifPresent(x -> helper.add("isFlagged", x));
        isUnread.ifPresent(x -> helper.add("isUnread", x));
        isAnswered.ifPresent(x -> helper.add("isAnswered", x));
        isDraft.ifPresent(x -> helper.add("isDraft", x));
        isForwarded.ifPresent(x -> helper.add("isForwarded", x));
        hasAttachment.ifPresent(x -> helper.add("hasAttachment", x));
        text.ifPresent(x -> helper.add("text", x));
        from.ifPresent(x -> helper.add("from", x));
        to.ifPresent(x -> helper.add("to", x));
        cc.ifPresent(x -> helper.add("cc", x));
        bcc.ifPresent(x -> helper.add("bcc", x));
        subject.ifPresent(x -> helper.add("subject", x));
        body.ifPresent(x -> helper.add("body", x));
        attachments.ifPresent(x -> helper.add("attachments", x));
        header.ifPresent(x -> helper.add("header", x));
        hasKeyword.ifPresent(x -> helper.add("hasKeyword", x));
        notKeyword.ifPresent(x -> helper.add("notKeyword", x));
        attachmentFileName.ifPresent(x -> helper.add("attachmentFileName", x));
        return helper.toString();
    }

    @Override
    public String prettyPrint(String indentation) {
        return indentation + toString() + "\n";
    }
}
