const qB = {
  randomProduct: function({
                            minComponents,
                            amount,
                            single,
                            connectedByTags,
                            minSimilarTags,
                            doNotTrim,
                            withPhoto
                          }, log, isTest) {
    let filtered = Object.values(dP.products);


    filtered = filtered
      .filter((p)=>p.getComponents().length >= minComponents);

    log.push(`Have >= ${minComponents} components: `+filtered.length);

    if(!filtered.length)
      return;

    if(single){
      amount = 1;
    }

    let minAmount, maxAmount;

    if(typeof amount !== 'number'){
      minAmount = amount.from;
      maxAmount = amount.to;
    }else{
      minAmount = maxAmount = amount;
    }




    let similarClusters = [];
    if(connectedByTags){
      let tagsHash = {};
      filtered.forEach(p => {
        p.getTags().forEach(({id})=>
          (tagsHash[id] || (tagsHash[id] = [])).push(p)
        );
      });

      let similarTags = {};
      filtered.forEach(p => {
        const tags = p.getTags(),
              tagsStr = tags.map(({id})=>id).sort().join(',');
        if(tagsStr in similarTags)
          return;

        let similar = filtered
            .map(subP =>
              ({
                similar: subP.getTags().filter(subT => tags.indexOf(subT)>-1).length,
                p: subP
              })
            )
            .filter(i=>i.similar>0)
            .sort((a, b)=>b.similar - a.similar);

        if(similar.length){
          let maxCount = similar[ 0 ].similar;
          let minCount = minSimilarTags || Math.round( maxCount - maxCount / 4 );

          similarTags[ tagsStr ] = similar
            .filter( s => s.similar >= minCount )
            .map(s=>s.p)
        }

      });

      similarClusters = Object.keys(similarTags)
        .map(k=>({k, v:similarTags[k]}))
        .filter( a => a.v.length >= minAmount && a.v.filter(isTest).length>0);


      log.push(`Clusters that have >= ${minSimilarTags} similar tags: `+Object.values(similarClusters).length);

      let randed = rand(similarClusters);

      log.push(`Used cluster tags: ${randed.k.split(',').map(id=>dP.tagsHash[id].name).join(', ')}`);

      filtered = randed.v;
    }


    let result;
    if(single){
      result = rand(filtered.filter(isTest));
      log.push('Matched product: '+result.title);

    }else{
      if(filtered.length < minAmount){
        log.push(`Not enough components meeting criteria`);
        return false;
      }

      let used = rand(filtered.filter(isTest)),
            other = filtered.slice();
      other.splice(other.indexOf(used));

      result = shuffle([used].concat(other).slice(0, doNotTrim? filtered.length: rand(minAmount, maxAmount)));
      log.push('Base products:');
      result.forEach(p => log.push(`  > ${p.title}`));
      log.push('');
    }

    return result;
  },

  subdivideComponents: function(from, sub) {
    const hash = {};
    from.forEach(c=>hash[normalizeText(c.name)] = c);
    sub.forEach(c=>delete hash[normalizeText(c.name)]);
    return Object.values(hash);

  },

  getUniqComponents: function(products) {
    return Object.values(qB.getUniqComponentsHash(products));
  },
  getUniqComponentsHash: function(products) {
    const hash = {};
    products.forEach(
      p=>
        p
          .getComponents()
          .forEach(c=>
            hash[normalizeText(c.name)] = c
          )

    );
    return hash;
  },

  getComponentsWithSharedProducts: function(
    products,
    minSharedProductCount,
    maxSharedProductCount
  ) {
    const hash = {};
    minSharedProductCount = minSharedProductCount || 0;
    maxSharedProductCount = maxSharedProductCount || Infinity;
    products.forEach(
      p=>
        p
          .getComponents()
          .forEach(c=> {
            const name = normalizeText( c.name );
            (hash[ name ] || (hash[name] = [])).push({c, p});
          })

    );
    return Object
      .values(hash)
      .filter(i =>
        i.length >= minSharedProductCount &&
        i.length <= maxSharedProductCount
      )
      .map(i=>({component: i[0].c, products: i.map(({p})=>p)}));
  },

  getComponentsNotInCorrect: function(income) {
    return shuffle(
      qB.subdivideComponents(
        qB.getUniqComponents(income.wrong),
        income.correct.getComponents()
      )
    );
  },

  getProductsWithTags: function({
    products,
    tags,
    minMatch
  }){
    tags = tags.filter(t=>t.type_id===1);
    if(minMatch === void 0){
      minMatch = tags.length;
    }
    const tHash = tags.reduce((s, t)=>{s[t.id] = t; return s;}, {});
    products = products || Object.values( dP.products );

    return products
      .filter(
        p => p.getTags()
          .filter(t=>t.id in tHash).length >= minMatch
      );
  },

  prebuild: {
    similarTaggedProductWithPhotoAndComponents: {
      fn: function(log, isTest) {
        this.getEmAll = true;
        this.products = {min: 2};
        const possibilities = shuffle(
          qB.prebuild.similarTaggedProductWithPhoto.fn.call(this, log, isTest)
        );
        if(!possibilities || !possibilities.length)
          return false;

        for( let i = 0, _i = possibilities.length; i < _i; i++ ){
          const possibility = possibilities[ i ];
          const components = possibility.correct.getComponents();
          const other = qB.getUniqComponentsHash(possibility.all.filter( sP => sP !== possibility.correct ))
          components.forEach(c=>delete other[normalizeText(c.name)]);

          if(Object.keys(other).length >= this.answers.min){
            log.push('Take '+possibility.baseProduct.title+' with uniq components:');
            components.forEach(c=>
              log.push(`  > ${c.name}`)
            );

            log.push('');
            log.push('Other components in group tagged as '+
              possibility.baseProduct.getTags().map(t=>t.name).join(', ')+':'
            );
            Object.values( other ).forEach(c=>
              log.push(`  > ${c.name}`)
            );

            const c = rand( components )
            return {
              allCorrect: components,
              baseProduct: possibility.baseProduct,
              correct: c,
              wrong: shuffle( Object.values( other ) ).slice( 0, rand( this.answers.min - 1, this.answers.max - 1 ) ),
              allWrong: Object.values( other )
            };
          }
        }
        return false;
      }
    },
    similarTaggedProductWithPhoto: {
      questionMinComponentsCount: Number,

      products: {min: Number, max: Number},
      fn: function(log, isTest) {
        let allProducts = Object.values(dP.products);
        let products = Object.values(dP.products)
          .filter(p=>p.image)
          .filter(p=>p.getComponents().length>=this.questionMinComponentsCount)

        log.push( `Products with image and >= ${this.questionMinComponentsCount} components count: ${products.length}` );
        if(products.length === 0){
          rand(Object.values(dP.products)).image = 'https://robohash.org/'+Math.random().toString(36)
          return false;
        }

        const possibility = products.map(p=>{
          if(!isTest(p))
            return false;
          const similar = qB.getProductsWithTags({
            products: allProducts,
            tags: p.getTags(),
            minMatch: 1
          });
          if(similar.length+1 >= this.products.min ){
            return {
              correct: p,
              baseProduct: p,
              wrong: similar.filter( sP => sP !== p ).slice(0,rand( this.products.min - 1,this.products.max - 1)),
              all: similar
            };
          }else{
            return false;
          }
        })
          .filter(Boolean);
        log.push( `Possible count: ${possibility.length}` );

        return this.getEmAll ? possibility : rand(possibility);
      }
    },
    similarTaggedProducts: {
      // minimal components in product
      questionCmpAmount: 4,

      // amount of matched products
      products: {from: 4, to: 8},

      // product similarity (minimal matched tags count)
      minSimilarTags: 2,

      fn(log, isTest){
        let products = qB.randomProduct({
          minComponents: this.questionCmpAmount,
          amount: this.products,
          connectedByTags: true,
          minSimilarTags: this.minSimilarTags
        }, log, isTest);

        for( let i = 0, _i = products.length; i < _i; i++ ){

          const product = products[ i ];
/*          if(this.withPhoto && !product.image)
            continue;*/
          if(!isTest(product))
            continue;
          const  cmps = product
              .getComponents()
              .filter(cmp => products.filter(p=>p.containsComponent(cmp)).length === 1)

          if(cmps.length>=this.questionCmpAmount){

            if(this.doNotLogUnique){
              log.push( `Product ${lapk( product.title )} have ${cmps.length} uniq components:` );
            }else{
              log.push( `Product ${lapk( product.title )} have >= ${this.questionCmpAmount} uniq components (${cmps.length}):` );
            }

            const shuffled = shuffle(cmps.slice()),
              used = shuffled.slice(0, this.questionCmpAmount);

            cmps.forEach(c=>log.push(`${!this.doNotLogUnique && used.indexOf(c)>-1?'+':' '} > ${c.name}`));

            let wrong = products.slice();
            wrong.splice(i,1);

            return {
              baseProduct: product,
              correct: product,
              wrong: wrong,
              uniq: used,
              allUniq: shuffled
            }
          }
        }
        return false;
      }
    }
  }
};