/*    Copyright 2014 MongoDB Inc.
 *
 *    Licensed 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.
 */

#pragma once

#include "mongo/db/jsobj.h"
#include "mongo/s/bson_serializable.h"

namespace mongo {

    /**
     * ChunkVersions consist of a major/minor version scoped to a version epoch
     *
     * Version configurations (format: major version, epoch):
     *
     * 1. (0, 0) - collection is dropped.
     * 2. (0, n), n > 0 - applicable only to shardVersion; shard has no chunk.
     * 3. (n, 0), n > 0 - invalid configuration.
     * 4. (n, m), n > 0, m > 0 - normal sharded collection version.
     *
     * TODO: This is a "manual type" but, even so, still needs to comform to what's
     * expected from types.
     */
    struct ChunkVersion : public BSONSerializable {

        union {
            struct {
                int _minor;
                int _major;
            };
            unsigned long long _combined;
        };
        OID _epoch;

        ChunkVersion() : _minor(0), _major(0), _epoch(OID()) {}

        //
        // Constructors shouldn't have default parameters here, since it's vital we track from
        // here on the epochs of versions, even if not used.
        //

        ChunkVersion( int major, int minor, const OID& epoch )
            : _minor(minor),_major(major), _epoch(epoch) {
        }

        ChunkVersion( unsigned long long ll, const OID& epoch )
            : _combined( ll ), _epoch(epoch) {
        }

        static ChunkVersion DROPPED() {
            return ChunkVersion( 0, 0, OID() ); // dropped OID is zero time, zero machineId/inc
        }

        static ChunkVersion UNSHARDED() {
            // TODO: Distinguish between these cases
            return DROPPED();
        }

        static ChunkVersion IGNORED() {
            ChunkVersion version = ChunkVersion();
            version._epoch.init( 0, true ); // ignored OID is zero time, max machineId/inc
            return version;
        }

        static bool isDroppedVersion( const ChunkVersion& version ) {
            return version.majorVersion() == 0 && version.minorVersion() == 0
                   && version.epoch() == DROPPED().epoch();
        }

        static bool isUnshardedVersion( const ChunkVersion& version ) {
            return isDroppedVersion( version );
        }

        static bool isIgnoredVersion( const ChunkVersion& version ) {
            return version.majorVersion() == 0 && version.minorVersion() == 0
                   && version.epoch() == IGNORED().epoch();
        }

        void inc( bool major ) {
            if ( major )
                incMajor();
            else
                incMinor();
        }

        void incMajor() {
            _major++;
            _minor = 0;
        }

        void incMinor() {
            _minor++;
        }

        // Incrementing an epoch creates a new, randomly generated identifier
        void incEpoch() {
            _epoch = OID::gen();
            _major = 0;
            _minor = 0;
        }

        // Note: this shouldn't be used as a substitute for version except in specific cases -
        // epochs make versions more complex
        unsigned long long toLong() const {
            return _combined;
        }

        bool isSet() const {
            return _combined > 0;
        }

        bool isEpochSet() const {
            return _epoch.isSet();
        }

        string toString() const {
            stringstream ss;
            // Similar to month/day/year.  For the most part when debugging, we care about major
            // so it's first
            ss << _major << "|" << _minor << "||" << _epoch;
            return ss.str();
        }

        int majorVersion() const { return _major; }
        int minorVersion() const { return _minor; }
        OID epoch() const { return _epoch; }

        //
        // Explicit comparison operators - versions with epochs have non-trivial comparisons.
        // > < operators do not check epoch cases.  Generally if using == we need to handle
        // more complex cases.
        //

        bool operator>( const ChunkVersion& otherVersion ) const {
            return this->_combined > otherVersion._combined;
        }

        bool operator>=( const ChunkVersion& otherVersion ) const {
            return this->_combined >= otherVersion._combined;
        }

        bool operator<( const ChunkVersion& otherVersion ) const {
            return this->_combined < otherVersion._combined;
        }

        bool operator<=( const ChunkVersion& otherVersion ) const {
            return this->_combined <= otherVersion._combined;
        }

        //
        // Equivalence comparison types.
        //

        // Can we write to this data and not have a problem?
        bool isWriteCompatibleWith( const ChunkVersion& otherVersion ) const {
            if( ! hasCompatibleEpoch( otherVersion ) ) return false;
            return otherVersion._major == _major;
        }

        // Is this the same version?
        bool isEquivalentTo( const ChunkVersion& otherVersion ) const {
            if( ! hasCompatibleEpoch( otherVersion ) ) return false;
            return otherVersion._combined == _combined;
        }

        /**
         * Returns true if the otherVersion is the same as this version and enforces strict epoch
         * checking (empty epochs are not wildcards).
         */
        bool isStrictlyEqualTo( const ChunkVersion& otherVersion ) const {
            if ( otherVersion._epoch != _epoch )
                return false;
            return otherVersion._combined == _combined;
        }

        /**
         * Returns true if this version is (strictly) in the same epoch as the other version and
         * this version is older.  Returns false if we're not sure because the epochs are different
         * or if this version is newer.
         */
        bool isOlderThan( const ChunkVersion& otherVersion ) const {
            if ( otherVersion._epoch != _epoch )
                return false;
            
            if ( _major != otherVersion._major )
                return _major < otherVersion._major;
	
            return _minor < otherVersion._minor;
        }

        // Is this in the same epoch?
        bool hasCompatibleEpoch( const ChunkVersion& otherVersion ) const {
            return hasCompatibleEpoch( otherVersion._epoch );
        }

        bool hasCompatibleEpoch( const OID& otherEpoch ) const {
            // TODO : Change logic from eras are not-unequal to eras are equal
            if( otherEpoch.isSet() && _epoch.isSet() && otherEpoch != _epoch ) return false;
            return true;
        }

        //
        // BSON input/output
        //
        // The idea here is to make the BSON input style very flexible right now, so we
        // can then tighten it up in the next version.  We can accept either a BSONObject field
        // with version and epoch, or version and epoch in different fields (either is optional).
        // In this case, epoch always is stored in a field name of the version field name + "Epoch"
        //

        //
        // { version : <TS> } and { version : [<TS>,<OID>] } format
        //

        static bool canParseBSON( const BSONElement& el, const string& prefix="" ){
            bool canParse;
            fromBSON( el, prefix, &canParse );
            return canParse;
        }

        static ChunkVersion fromBSON( const BSONElement& el, const string& prefix="" ){
            bool canParse;
            return fromBSON( el, prefix, &canParse );
        }

        static ChunkVersion fromBSON( const BSONElement& el,
                                      const string& prefix,
                                      bool* canParse )
        {
            *canParse = true;

            int type = el.type();

            if( type == Array ){
                return fromBSON( BSONArray( el.Obj() ), canParse );
            }

            if( type == jstOID ){
                return ChunkVersion( 0, 0, el.OID() );
            }

            if( el.isNumber() ){
                return ChunkVersion( static_cast<unsigned long long>(el.numberLong()), OID() );
            }

            if( type == Timestamp || type == Date ){
                return ChunkVersion( el._numberLong(), OID() );
            }

            *canParse = false;

            return ChunkVersion( 0, OID() );
        }

        //
        // { version : <TS>, versionEpoch : <OID> } object format
        //

        static bool canParseBSON( const BSONObj& obj, const string& prefix="" ){
            bool canParse;
            fromBSON( obj, prefix, &canParse );
            return canParse;
        }

        static ChunkVersion fromBSON( const BSONObj& obj, const string& prefix="" ){
            bool canParse;
            return fromBSON( obj, prefix, &canParse );
        }

        static ChunkVersion fromBSON( const BSONObj& obj,
                                      const string& prefixIn,
                                      bool* canParse )
        {
            *canParse = true;

            string prefix = prefixIn;
            // "version" doesn't have a "cluster constanst" because that field is never
            // written to the config.
            if( prefixIn == "" && ! obj[ "version" ].eoo() ){
                prefix = (string)"version";
            }
            // TODO: use ChunkType::DEPRECATED_lastmod()
            // NOTE: type_chunk.h includes this file
            else if( prefixIn == "" && ! obj["lastmod"].eoo() ){
                prefix = (string)"lastmod";
            }

            ChunkVersion version = fromBSON( obj[ prefix ], prefixIn, canParse );

            if( obj[ prefix + "Epoch" ].type() == jstOID ){
                version._epoch = obj[ prefix + "Epoch" ].OID();
                *canParse = true;
            }

            return version;
        }

        //
        // { version : [<TS>, <OID>] } format
        //

        static bool canParseBSON( const BSONArray& arr ){
            bool canParse;
            fromBSON( arr, &canParse );
            return canParse;
        }

        static ChunkVersion fromBSON( const BSONArray& arr ){
            bool canParse;
            return fromBSON( arr, &canParse );
        }

        static ChunkVersion fromBSON( const BSONArray& arr,
                                      bool* canParse )
        {
            *canParse = false;

            ChunkVersion version;

            BSONObjIterator it( arr );
            if( ! it.more() ) return version;

            version = fromBSON( it.next(), "", canParse );
            if( ! (*canParse) ) return version;

            *canParse = true;

            if( ! it.more() ) return version;
            BSONElement next = it.next();
            if( next.type() != jstOID ) return version;

            version._epoch = next.OID();

            return version;
        }

        enum VersionChoice {
            VersionChoice_Local,
            VersionChoice_Remote,
            VersionChoice_Unknown
        };

        /**
         * Compares a remotely-loaded version 'remoteVersion' to the latest local version of a
         * collection, 'localVersion', and returns the newest.
         *
         * Because it isn't clear during epoch changes which epoch is newer, the local version
         * before the reload occurred, 'prevLocalVersion', is used to determine whether the remote
         * epoch is definitely newer, or we're not sure.
         */
        static VersionChoice chooseNewestVersion( ChunkVersion prevLocalVersion,
                                                  ChunkVersion localVersion,
                                                  ChunkVersion remoteVersion )
        {
            OID prevEpoch = prevLocalVersion.epoch();
            OID localEpoch = localVersion.epoch();
            OID remoteEpoch = remoteVersion.epoch();

            // Everything changed in-flight, so we need to try again
            if ( prevEpoch != localEpoch && localEpoch != remoteEpoch ) {
                return VersionChoice_Unknown;
            }

            // We're in the same (zero) epoch as the latest metadata, nothing to do
            if ( localEpoch == remoteEpoch && !remoteEpoch.isSet() ) {
                return VersionChoice_Local;
            }

            // We're in the same (non-zero) epoch as the latest metadata, so increment the version
            if ( localEpoch == remoteEpoch && remoteEpoch.isSet() ) {

                // Use the newer version if possible
                if ( localVersion < remoteVersion ) {
                    return VersionChoice_Remote;
                }
                else {
                    return VersionChoice_Local;
                }
            }

            // We're now sure we're installing a new epoch and the epoch didn't change during reload
            dassert( prevEpoch == localEpoch && localEpoch != remoteEpoch );
            return VersionChoice_Remote;
        }

        //
        // Currently our BSON output is to two different fields, to cleanly work with older
        // versions that know nothing about epochs.
        //

        BSONObj toBSONWithPrefix( const string& prefixIn ) const {
            BSONObjBuilder b;

            string prefix = prefixIn;
            if( prefix == "" ) prefix = "version";

            b.appendTimestamp( prefix, _combined );
            b.append( prefix + "Epoch", _epoch );
            return b.obj();
        }

        void addToBSON( BSONObjBuilder& b, const string& prefix="" ) const {
            b.appendElements( toBSONWithPrefix( prefix ) );
        }

        void addEpochToBSON( BSONObjBuilder& b, const string& prefix="" ) const {
            b.append( prefix + "Epoch", _epoch );
        }

        //
        // bson serializable interface implementation
        // (toBSON and toString were implemented above)
        //

        virtual bool isValid(std::string* errMsg) const {
            // TODO is there any check we want to do here?
            return true;
        }

        virtual BSONObj toBSON() const {
            // ChunkVersion wants to be an array.
            BSONArrayBuilder b;
            b.appendTimestamp(_combined);
            b.append(_epoch);
            return b.arr();
        }

        virtual bool parseBSON(const BSONObj& source, std::string* errMsg) {
            // ChunkVersion wants to be an array.
            BSONArray arrSource = static_cast<BSONArray>(source);

            bool canParse;
            ChunkVersion version = fromBSON(arrSource, &canParse);
            if (!canParse) {
                *errMsg = "Could not parse version structure";
                return false;
            }

            _minor = version._minor;
            _major = version._major;
            _epoch = version._epoch;
            return true;
        }

        virtual void clear() {
            _minor = 0;
            _major = 0;
            _epoch = OID();
        }

        void cloneTo(ChunkVersion* other) const {
            other->clear();
            other->_minor = _minor;
            other->_major = _major;
            other->_epoch = _epoch;
        }

    };

    inline ostream& operator<<( ostream &s , const ChunkVersion& v) {
        s << v.toString();
        return s;
    }

} // namespace mongo
