/*
 * 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.nifi.controller.repository;

import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.nifi.controller.repository.claim.ContentClaim;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.flowfile.attributes.CoreAttributes;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * <p>
 * A flow file is a logical notion of an item in a flow with its associated attributes and identity which can be used as a reference for its actual content.
 * </p>
 *
 * <b>Immutable - Thread Safe</b>
 *
 */
public final class StandardFlowFileRecord implements FlowFile, FlowFileRecord {

    private final long id;
    private final long entryDate;
    private final long lineageStartDate;
    private final long lineageStartIndex;
    private final long size;
    private final long penaltyExpirationMs;
    private final Map<String, String> attributes;
    private final ContentClaim claim;
    private final long claimOffset;
    private final long lastQueueDate;
    private final long queueDateIndex;

    private StandardFlowFileRecord(final Builder builder) {
        this.id = builder.bId;
        this.attributes = builder.bAttributes == null ? Collections.emptyMap() : builder.bAttributes;
        this.entryDate = builder.bEntryDate;
        this.lineageStartDate = builder.bLineageStartDate;
        this.lineageStartIndex = builder.bLineageStartIndex;
        this.penaltyExpirationMs = builder.bPenaltyExpirationMs;
        this.size = builder.bSize;
        this.claim = builder.bClaim;
        this.claimOffset = builder.bClaimOffset;
        this.lastQueueDate = builder.bLastQueueDate;
        this.queueDateIndex = builder.bQueueDateIndex;
    }

    @Override
    public long getId() {
        return id;
    }

    @Override
    public long getEntryDate() {
        return entryDate;
    }

    @Override
    public long getLineageStartDate() {
        return lineageStartDate;
    }

    @Override
    public Long getLastQueueDate() {
        return lastQueueDate;
    }

    @Override
    public boolean isPenalized() {
        return penaltyExpirationMs > 0 ? penaltyExpirationMs > System.currentTimeMillis() : false;
    }

    @Override
    public String getAttribute(final String key) {
        return attributes.get(key);
    }

    @Override
    public long getSize() {
        return size;
    }

    @Override
    public Map<String, String> getAttributes() {
        return Collections.unmodifiableMap(this.attributes);
    }

    @Override
    public ContentClaim getContentClaim() {
        return this.claim;
    }

    @Override
    public long getContentClaimOffset() {
        return this.claimOffset;
    }

    @Override
    public long getLineageStartIndex() {
        return lineageStartIndex;
    }

    @Override
    public long getQueueDateIndex() {
        return queueDateIndex;
    }

    /**
     * Provides the natural ordering for FlowFile objects which is based on their identifier.
     *
     * @param other other
     * @return standard compare contract
     */
    @Override
    public int compareTo(final FlowFile other) {
        return new CompareToBuilder().append(id, other.getId()).toComparison();
    }

    @Override
    public boolean equals(final Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof FlowFile)) {
            return false;
        }
        final FlowFile otherRecord = (FlowFile) other;
        return id == otherRecord.getId();
    }

    @Override
    public String toString() {
        final ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
        builder.append("uuid", getAttribute(CoreAttributes.UUID.key()));
        builder.append("claim", claim == null ? "" : claim.toString());
        builder.append("offset", claimOffset);
        builder.append("name", getAttribute(CoreAttributes.FILENAME.key())).append("size", size);
        return builder.toString();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(7, 13).append(id).toHashCode();
    }

    public static final class Builder {

        private long bId;
        private long bEntryDate = System.currentTimeMillis();
        private long bLineageStartDate = bEntryDate;
        private long bLineageStartIndex = 0L;
        private final Set<String> bLineageIdentifiers = new HashSet<>();
        private long bPenaltyExpirationMs = -1L;
        private long bSize = 0L;
        private ContentClaim bClaim = null;
        private long bClaimOffset = 0L;
        private long bLastQueueDate = System.currentTimeMillis();
        private long bQueueDateIndex = 0L;
        private Map<String, String> bAttributes;
        private boolean bAttributesCopied = false;

        public Builder id(final long id) {
            bId = id;
            return this;
        }

        public Builder entryDate(final long epochMs) {
            bEntryDate = epochMs;
            return this;
        }

        public Builder lineageStart(final long lineageStartDate, final long lineageStartIndex) {
            bLineageStartDate = lineageStartDate;
            bLineageStartIndex = lineageStartIndex;
            return this;
        }

        public Builder penaltyExpirationTime(final long epochMilliseconds) {
            bPenaltyExpirationMs = epochMilliseconds;
            return this;
        }

        public Builder size(final long bytes) {
            if (bytes >= 0) {
                bSize = bytes;
            }
            return this;
        }

        private Map<String, String> initializeAttributes() {
            if (bAttributes == null) {
                bAttributes = new HashMap<>();
                bAttributesCopied = true;
            } else if (!bAttributesCopied) {
                bAttributes = new HashMap<>(bAttributes);
                bAttributesCopied = true;
            }

            return bAttributes;
        }

        public Builder addAttribute(final String key, final String value) {
            if (key != null && value != null) {
                initializeAttributes().put(FlowFile.KeyValidator.validateKey(key), value);
            }
            return this;
        }

        public Builder addAttributes(final Map<String, String> attributes) {
            final Map<String, String> initializedAttributes = initializeAttributes();

            if (null != attributes) {
                for (final String key : attributes.keySet()) {
                    FlowFile.KeyValidator.validateKey(key);
                }
                for (final Map.Entry<String, String> entry : attributes.entrySet()) {
                    final String key = entry.getKey();
                    final String value = entry.getValue();
                    if (key != null && value != null) {
                        initializedAttributes.put(key, value);
                    }
                }
            }
            return this;
        }

        public Builder removeAttributes(final String... keys) {
            if (keys != null) {
                for (final String key : keys) {
                    if (CoreAttributes.UUID.key().equals(key)) {
                        continue;
                    }

                    initializeAttributes().remove(key);
                }
            }
            return this;
        }

        public Builder removeAttributes(final Set<String> keys) {
            if (keys != null) {
                for (final String key : keys) {
                    if (CoreAttributes.UUID.key().equals(key)) {
                        continue;
                    }

                    initializeAttributes().remove(key);
                }
            }
            return this;
        }

        public Builder removeAttributes(final Pattern keyPattern) {
            if (keyPattern != null) {
                final Iterator<String> iterator = initializeAttributes().keySet().iterator();
                while (iterator.hasNext()) {
                    final String key = iterator.next();

                    if (CoreAttributes.UUID.key().equals(key)) {
                        continue;
                    }

                    if (keyPattern.matcher(key).matches()) {
                        iterator.remove();
                    }
                }
            }
            return this;
        }

        public Builder contentClaim(final ContentClaim claim) {
            this.bClaim = claim;
            return this;
        }

        public Builder contentClaimOffset(final long offset) {
            this.bClaimOffset = offset;
            return this;
        }

        public Builder lastQueued(final long lastQueueDate, final long queueDateIndex) {
            this.bLastQueueDate = lastQueueDate;
            this.bQueueDateIndex = queueDateIndex;
            return this;
        }

        public Builder fromFlowFile(final FlowFileRecord specFlowFile) {
            if (specFlowFile == null) {
                return this;
            }
            bId = specFlowFile.getId();
            bEntryDate = specFlowFile.getEntryDate();
            bLineageStartDate = specFlowFile.getLineageStartDate();
            bLineageStartIndex = specFlowFile.getLineageStartIndex();
            bLineageIdentifiers.clear();
            bPenaltyExpirationMs = specFlowFile.getPenaltyExpirationMillis();
            bSize = specFlowFile.getSize();
            // If this is a StandardFlowFileRecord, access the attributes map directly. Do not use the
            // getAttributes() method, because that will wrap the original in an UnmodifiableMap. As a result,
            // a Processor that continually calls session.append() for instance will have a FlowFile whose attributes
            // Map is wrapped thousands of times until it hits a StackOverflowError. We want the getter to return
            // UnmodifiableMap, though, so that Processors cannot directly modify that Map.
            bAttributes = specFlowFile instanceof StandardFlowFileRecord ? ((StandardFlowFileRecord) specFlowFile).attributes : specFlowFile.getAttributes();
            bAttributesCopied = false;
            bClaim = specFlowFile.getContentClaim();
            bClaimOffset = specFlowFile.getContentClaimOffset();
            bLastQueueDate = specFlowFile.getLastQueueDate();
            bQueueDateIndex = specFlowFile.getQueueDateIndex();

            return this;
        }

        public FlowFileRecord build() {
            return new StandardFlowFileRecord(this);
        }
    }

    @Override
    public long getPenaltyExpirationMillis() {
        return penaltyExpirationMs;
    }
}
