// @flow
import countBy from 'lodash/countBy';
import differenceBy from 'lodash/differenceBy';
import find from 'lodash/find';
import groupBy from 'lodash/groupBy';
import intersectionBy from 'lodash/intersectionBy';
import get from 'lodash/get';
import keyBy from 'lodash/keyBy';
import map from 'lodash/map';
import pullAllBy from 'lodash/pullAllBy';
import unionBy from 'lodash/unionBy';
import uniqBy from 'lodash/uniqBy';
import clone from 'lodash/clone';
import isEqual from 'lodash/isEqual';
import range from 'lodash/range';
import sortedIndexBy from 'lodash/sortedIndexBy';
import sortedLastIndexBy from 'lodash/sortedLastIndexBy';
import extend from 'lodash/extend';
import assignIn from 'lodash/assignIn';
import xorBy from 'lodash/xorBy';
import zip from 'lodash/zip';
import isString from 'lodash/isString';
import noop from 'lodash/noop';
import concat from 'lodash/concat';
import first from 'lodash/first';
import conformsTo from 'lodash/conformsTo';
import defaultTo from 'lodash/defaultTo';
import tap from 'lodash/tap';
import thru from 'lodash/thru';
import times from 'lodash/times';
import toPairs from "lodash/toPairs";
import toPairsIn from "lodash/toPairsIn";
import flatMap from 'lodash/flatMap';

/**
 * _.countBy
 */
countBy([6.1, 4.2, 6.3], Math.floor);
countBy(['one', 'two', 'three'], 'length');


/**
 * _.differenceBy
 */
differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');


/**
 * _.find
 */
find([1, 2, 3], x => x * 1 == 3);
find([1, 2, 3], x => x == 2, 1);
// $FlowExpectedError number cannot be compared to string
find([1, 2, 3], x => x == 'a');
// $FlowExpectedError number. This type is incompatible with function type.
find([1, 2, 3], 1);
// $FlowExpectedError property `y`. Property not found in object literal
find([{x:1}, {x:2}, {x:3}], v => v.y == 3);
find([{x:1}, {x:2}, {x:3}], v => v.x == 3);
find({x: 1, y: 2}, (a: number, b: string) => a);
find({x: 1, y: 2}, { x: 3 });

// $FlowExpectedError undefined. This type is incompatible with object type.
var result: Object = find(users, 'active');

/**
 * _.find examples from the official doc
 */
var users = [
  { 'user': 'barney',  'age': 36, 'active': true },
  { 'user': 'fred',    'age': 40, 'active': false },
  { 'user': 'pebbles', 'age': 1,  'active': true }
];

find(users, function(o) { return o.age < 40; });

// The `_.matches` iteratee shorthand.
find(users, { 'age': 1, 'active': true });

// The `_.matchesProperty` iteratee shorthand.
find(users, ['active', false]);

// The `_.property` iteratee shorthand.
find(users, 'active');


/**
 * _.groupBy
 */
var numbersGroupedByMathFloor = groupBy([6.1, 4.2, 6.3], Math.floor);
if (numbersGroupedByMathFloor[6]) {
  numbersGroupedByMathFloor[6][0] / numbersGroupedByMathFloor[6][1]
}
var stringsGroupedByLength = groupBy(['one', 'two', 'three'], 'length');
if (stringsGroupedByLength[3]) {
  stringsGroupedByLength[3][0].toLowerCase();
}
var numbersObj: {[key: string]: number} = {a: 6.1, b: 4.2, c: 6.3};
var numbersGroupedByMathFloor2 = groupBy(numbersObj, Math.floor);
if (numbersGroupedByMathFloor2[6]) {
  numbersGroupedByMathFloor2[6][0] / numbersGroupedByMathFloor2[6][1]
}
var stringObj: {[key: string]: string} = {a: 'one', b: 'two', c: 'three'};
var stringsGroupedByLength2 = groupBy(stringObj, 'length');
if (stringsGroupedByLength2[3]) {
  stringsGroupedByLength2[3][0].toLowerCase();
}


/**
 * _.intersectionBy
 */
intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);
intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');


/**
 * _.get
 */

// Object — examples from lodash docs
var exampleObjectForGetTest = { 'a': [{ 'b': { 'c': 3 } }] };
get(exampleObjectForGetTest, 'a[0].b.c');
get(exampleObjectForGetTest, ['a', '0', 'b', 'c']);
get(exampleObjectForGetTest, 'a.b.c', 'default');

// Array — not documented, but _.get does support arrays
get([1, 2, 3], '0');
get(['foo', 'bar', 'baz'], '[1]');
get([{ a: 'foo' }, { b: 'bar' }, { c: 'baz' }], '2');
get([[1, 2], [3, 4], [5, 6], [7, 8]], '3');

// Second argument must be string when looking for array items by index
// $FlowExpectedError number This type is incompatible with union: ?array type | string
get([1, 2, 3], 0);


/**
 * _.keyBy
 */
keyBy([
  { 'dir': 'left', 'code': 97 },
  { 'dir': 'right', 'code': 100 }
], function(o) {
  return String.fromCharCode(o.code);
});
keyBy([
  { 'dir': 'left', 'code': 97 },
  { 'dir': 'right', 'code': 100 }
], 'dir');


/**
 * _.map examples from the official doc
 */
function square(n) {
  return n * n;
}

map([4, 8], square);
map({ 'a': 4, 'b': 8 }, square);

var users = [
  { 'user': 'barney' },
  { 'user': 'fred' }
];

// The `_.property` iteratee shorthand.
map(users, 'user');


/**
 * _.pullAllBy
 */
pullAllBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }], [{ 'x': 1 }, { 'x': 3 }], 'x');


/**
 * _.unionBy
 */
unionBy([2.1], [1.2, 2.3], Math.floor);
unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');


/**
 * _.uniqBy
 */
uniqBy([2.1, 1.2, 2.3], Math.floor);
uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');


/**
 * _.clone
 */
clone({a: 1}).a == 1;
// $FlowExpectedError property `b`. Property not found in object literal
clone({a: 1}).b == 1
// $FlowExpectedError number. This type is incompatible with function type.
clone({a: 1}).a == 'c';

/**
 * _.isEqual
 */
isEqual('a', 'b');
isEqual({x: 1}, {y: 2});

// Flow considers these compatible with isEqual(a: any, b: any).
// Reasonable people disagree about whether these should be considered legal calls.
// See https://github.com/splodingsocks/FlowTyped/pull/1#issuecomment-149345275
// and https://github.com/facebook/flow/issues/956
isEqual(1);
isEqual(1, 2, 3);


/**
 * _.range
 */
range(0, 10)[4] == 4
// $FlowExpectedError string. This type is incompatible with number
range(0, 'a');
// $FlowExpectedError string cannot be compared to number
range(0, 10)[4] == 'a';


/**
 * _.sortedIndexBy
 */
sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, function(o) { return o.x; });
sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');


/**
 * _.sortedLastIndexBy
 */
sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, function(o) { return o.x; });
sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');

/**
 * _.extend
 */
extend({a: 1}, {b: 2}).a
extend({a: 1}, {b: 2}).b
// $FlowExpectedError property `c`. Property not found in object literal
extend({a: 1}, {b: 2}).c
// $FlowExpectedError property `c`. Poperty not found in object literal
assignIn({a: 1}, {b: 2}).c


/**
 * _.xorBy
 */
xorBy([2.1, 1.2], [2.3, 3.4], Math.floor);
xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');


/**
 * _.zip
 */
zip(['a', 'b', 'c'], ['d', 'e', 'f'])[0].length;
zip(['a', 'b', 'c'], [1, 2, 3])[0].length;
zip(['a', 'b', 'c'], [1, 2, 3])[0][0] + 'a'
zip(['a', 'b', 'c'], [1, 2, 3])[0][1] * 10
// $FlowExpectedError `x` property not found in Array
zip([{x:1}], [{x:2,y:1}])[0].x
// $FlowExpectedError `y` property not found in object literal
zip([{x:1}], [{x:2,y:1}])[0][0].y
zip([{x:1}], [{x:2,y:1}])[0][1].y
// $FlowExpectedError Flow could potentially catch this -- the tuple only has two elements.
zip([{x:1}], [{x:2,y:1}])[0][2]

/**
 * _.isString
 */

var boolTrue: true;
var boolFalse: false;

boolTrue  = isString('foo');
boolFalse = isString(['']);
boolFalse = isString({});
boolFalse = isString(5);
boolFalse = isString(function(f) { return f });
boolFalse = isString();
boolFalse = isString(true);

// $FlowExpectedError
boolFalse = isString('');
// $FlowExpectedError
boolTrue = isString(undefined);


/**
 * _.find
 */
find([1, 2, 3], x => x == 1);
// $FlowExpectedError number. This type is incompatible with function type.
find([1, 2, 3], 1);


// Copy pasted tests from iflow-lodash
var nums : number[] = [1,2,3,4,5,6];
var num : number;
var string : string;
var bool : bool;

var nativeSquares : number[];
var directSquares : number[];

var nativeStrings : string[];
var directStrings : string[];

var allNums : number[];
var numsAndStrList : Array<number|string>;
var mixedList : Array<mixed>;
allNums = concat(nums, nums, nums);
numsAndStrList = concat(nums, '123', '456');
numsAndStrList = concat(nums, ['123', '456']);
numsAndStrList = concat(nums, [[1,2,3], '456']);
mixedList = concat(nums, [[1,2,3], '456']);

// Array#map, lodash.map, lodash#map
nativeSquares = nums.map(function(num) {
  return num * num;
});
directSquares = map(nums, function(num) {
  return num * num;
});

num = first(nums);

// return type of iterator is reflected in result and chain
nativeStrings = nums.map(function(num) {
  return JSON.stringify(num);
});
directStrings = map(nums, function(num) {
  return JSON.stringify(num);
});

var obj = {a:1, b:2};
bool = conformsTo(obj, {
  a: function(x:number) {
    return true;
  },
});

num = defaultTo(undefined, 2);
string = defaultTo(undefined, 'str');
bool = defaultTo(true, 'str');
string = defaultTo('str', true);

num = tap(1, function(n) { return false; });
bool = thru(1, function(n) { return false; });

var timesNums: number[];

timesNums = times(5);
// $FlowExpectedError string. This type is incompatible with number
var strings : string[] = times(5);
timesNums = times(5, function(i: number) { return i + 1; });
// $FlowExpectedError string. This type is incompatible with number
timesNums = times(5, function(i: number) { return JSON.stringify(i); });

// lodash.flatMap for collections and objects
// this arrow function needs a type annotation due to a bug in flow
// https://github.com/facebook/flow/issues/1948
flatMap([1, 2, 3], (n): number[] => [n, n]);
flatMap({a: 1, b: 2}, n => [n, n]);

/**
 * _.noop
 */
noop();
noop(1);
noop('a', 2, [], null);
(noop: (string) => void);
(noop: (number, string) => void);
// $FlowExpectedError functions are contravariant in return types
(noop: (string) => string);

/**
 * _.toPairs / _.toPairsIn
 */
var pairs: [string, number][];
pairs = toPairs({ a: 12, b: 100 });
pairs = toPairsIn({ a: 12, b: 100 });
